mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into tooling/storybook-poc
This commit is contained in:
@@ -17,11 +17,15 @@
|
||||
"lodash": "^4.17.10",
|
||||
"moment": "^2.22.2",
|
||||
"react": "^16.6.3",
|
||||
"react-custom-scrollbars": "^4.2.1",
|
||||
"react-dom": "^16.6.3",
|
||||
"react-highlight-words": "0.11.0",
|
||||
"react-popper": "^1.3.0",
|
||||
"react-transition-group": "^2.2.1",
|
||||
"react-virtualized": "^9.21.0"
|
||||
"react-virtualized": "^9.21.0",
|
||||
"tether": "^1.4.0",
|
||||
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
|
||||
"tinycolor2": "^1.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-info": "^4.1.4",
|
||||
@@ -33,8 +37,14 @@
|
||||
"@types/node": "^10.12.18",
|
||||
"@types/react": "^16.7.6",
|
||||
"@types/storybook__react": "^4.0.0",
|
||||
"@types/react-custom-scrollbars": "^4.0.5",
|
||||
"@types/react-test-renderer": "^16.0.3",
|
||||
"@types/react-transition-group": "^2.0.15",
|
||||
"@types/tether-drop": "^1.4.8",
|
||||
"@types/tinycolor2": "^1.4.1",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"react-docgen-typescript-webpack-plugin": "^1.1.0",
|
||||
"react-test-renderer": "^16.7.0",
|
||||
"typescript": "^3.2.2"
|
||||
},
|
||||
"resolutions": {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { ColorPalette } from './ColorPalette';
|
||||
|
||||
describe('CollorPalette', () => {
|
||||
it('renders correctly', () => {
|
||||
const tree = renderer.create(<ColorPalette color="#EAB839" onColorSelect={jest.fn()} />).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { sortedColors } from '../../utils';
|
||||
|
||||
export interface Props {
|
||||
color: string;
|
||||
onColorSelect: (c: string) => void;
|
||||
}
|
||||
|
||||
export class ColorPalette extends React.Component<Props, any> {
|
||||
paletteColors: string[];
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.paletteColors = sortedColors;
|
||||
this.onColorSelect = this.onColorSelect.bind(this);
|
||||
}
|
||||
|
||||
onColorSelect(color: string) {
|
||||
return () => {
|
||||
this.props.onColorSelect(color);
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const colorPaletteItems = this.paletteColors.map(paletteColor => {
|
||||
const cssClass = paletteColor.toLowerCase() === this.props.color.toLowerCase() ? 'fa-circle-o' : 'fa-circle';
|
||||
return (
|
||||
<i
|
||||
key={paletteColor}
|
||||
className={'pointer fa ' + cssClass}
|
||||
style={{ color: paletteColor }}
|
||||
onClick={this.onColorSelect(paletteColor)}
|
||||
>
|
||||
|
||||
</i>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className="graph-legend-popover">
|
||||
<p className="m-b-0">{colorPaletteItems}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Drop from 'tether-drop';
|
||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||
|
||||
export interface Props {
|
||||
color: string;
|
||||
onChange: (c: string) => void;
|
||||
}
|
||||
|
||||
export class ColorPicker extends React.Component<Props, any> {
|
||||
pickerElem: HTMLElement | null;
|
||||
colorPickerDrop: any;
|
||||
|
||||
openColorPicker = () => {
|
||||
const dropContent = <ColorPickerPopover color={this.props.color} onColorSelect={this.onColorSelect} />;
|
||||
|
||||
const dropContentElem = document.createElement('div');
|
||||
ReactDOM.render(dropContent, dropContentElem);
|
||||
|
||||
const drop = new Drop({
|
||||
target: this.pickerElem as Element,
|
||||
content: dropContentElem,
|
||||
position: 'top center',
|
||||
classes: 'drop-popover',
|
||||
openOn: 'click',
|
||||
hoverCloseDelay: 200,
|
||||
tetherOptions: {
|
||||
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
|
||||
attachment: 'bottom center',
|
||||
},
|
||||
});
|
||||
|
||||
drop.on('close', this.closeColorPicker);
|
||||
|
||||
this.colorPickerDrop = drop;
|
||||
this.colorPickerDrop.open();
|
||||
};
|
||||
|
||||
closeColorPicker = () => {
|
||||
setTimeout(() => {
|
||||
if (this.colorPickerDrop && this.colorPickerDrop.tether) {
|
||||
this.colorPickerDrop.destroy();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
onColorSelect = (color: string) => {
|
||||
this.props.onChange(color);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="sp-replacer sp-light" onClick={this.openColorPicker} ref={element => (this.pickerElem = element)}>
|
||||
<div className="sp-preview">
|
||||
<div className="sp-preview-inner" style={{ backgroundColor: this.props.color }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import $ from 'jquery';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { ColorPalette } from './ColorPalette';
|
||||
import { SpectrumPicker } from './SpectrumPicker';
|
||||
|
||||
const DEFAULT_COLOR = '#000000';
|
||||
|
||||
export interface Props {
|
||||
color: string;
|
||||
onColorSelect: (c: string) => void;
|
||||
}
|
||||
|
||||
export class ColorPickerPopover extends React.Component<Props, any> {
|
||||
pickerNavElem: any;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tab: 'palette',
|
||||
color: this.props.color || DEFAULT_COLOR,
|
||||
colorString: this.props.color || DEFAULT_COLOR,
|
||||
};
|
||||
}
|
||||
|
||||
setPickerNavElem(elem: any) {
|
||||
this.pickerNavElem = $(elem);
|
||||
}
|
||||
|
||||
setColor(color: string) {
|
||||
const newColor = tinycolor(color);
|
||||
if (newColor.isValid()) {
|
||||
this.setState({ color: newColor.toString(), colorString: newColor.toString() });
|
||||
this.props.onColorSelect(color);
|
||||
}
|
||||
}
|
||||
|
||||
sampleColorSelected(color: string) {
|
||||
this.setColor(color);
|
||||
}
|
||||
|
||||
spectrumColorSelected(color: any) {
|
||||
const rgbColor = color.toRgbString();
|
||||
this.setColor(rgbColor);
|
||||
}
|
||||
|
||||
onColorStringChange(e: any) {
|
||||
const colorString = e.target.value;
|
||||
this.setState({ colorString: colorString });
|
||||
|
||||
const newColor = tinycolor(colorString);
|
||||
if (newColor.isValid()) {
|
||||
// Update only color state
|
||||
const newColorString = newColor.toString();
|
||||
this.setState({ color: newColorString });
|
||||
this.props.onColorSelect(newColorString);
|
||||
}
|
||||
}
|
||||
|
||||
onColorStringBlur(e: any) {
|
||||
const colorString = e.target.value;
|
||||
this.setColor(colorString);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.pickerNavElem.find('li:first').addClass('active');
|
||||
this.pickerNavElem.on('show', (e: any) => {
|
||||
// use href attr (#name => name)
|
||||
const tab = e.target.hash.slice(1);
|
||||
this.setState({ tab: tab });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const paletteTab = (
|
||||
<div id="palette">
|
||||
<ColorPalette color={this.state.color} onColorSelect={this.sampleColorSelected.bind(this)} />
|
||||
</div>
|
||||
);
|
||||
const spectrumTab = (
|
||||
<div id="spectrum">
|
||||
<SpectrumPicker color={this.state.color} onColorSelect={this.spectrumColorSelected.bind(this)} options={{}} />
|
||||
</div>
|
||||
);
|
||||
const currentTab = this.state.tab === 'palette' ? paletteTab : spectrumTab;
|
||||
|
||||
return (
|
||||
<div className="gf-color-picker">
|
||||
<ul className="nav nav-tabs" id="colorpickernav" ref={this.setPickerNavElem.bind(this)}>
|
||||
<li className="gf-tabs-item-colorpicker">
|
||||
<a href="#palette" data-toggle="tab">
|
||||
Colors
|
||||
</a>
|
||||
</li>
|
||||
<li className="gf-tabs-item-colorpicker">
|
||||
<a href="#spectrum" data-toggle="tab">
|
||||
Custom
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="gf-color-picker__body">{currentTab}</div>
|
||||
<div>
|
||||
<input
|
||||
className="gf-form-input gf-form-input--small"
|
||||
value={this.state.colorString}
|
||||
onChange={this.onColorStringChange.bind(this)}
|
||||
onBlur={this.onColorStringBlur.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Drop from 'tether-drop';
|
||||
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
|
||||
|
||||
export interface SeriesColorPickerProps {
|
||||
color: string;
|
||||
yaxis?: number;
|
||||
optionalClass?: string;
|
||||
onColorChange: (newColor: string) => void;
|
||||
onToggleAxis?: () => void;
|
||||
}
|
||||
|
||||
export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
|
||||
pickerElem: any;
|
||||
colorPickerDrop: any;
|
||||
|
||||
static defaultProps = {
|
||||
optionalClass: '',
|
||||
yaxis: undefined,
|
||||
onToggleAxis: () => {},
|
||||
};
|
||||
|
||||
constructor(props: SeriesColorPickerProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.destroyDrop();
|
||||
}
|
||||
|
||||
onClickToOpen = () => {
|
||||
if (this.colorPickerDrop) {
|
||||
this.destroyDrop();
|
||||
}
|
||||
|
||||
const { color, yaxis, onColorChange, onToggleAxis } = this.props;
|
||||
const dropContent = (
|
||||
<SeriesColorPickerPopover color={color} yaxis={yaxis} onColorChange={onColorChange} onToggleAxis={onToggleAxis} />
|
||||
);
|
||||
const dropContentElem = document.createElement('div');
|
||||
ReactDOM.render(dropContent, dropContentElem);
|
||||
|
||||
const drop = new Drop({
|
||||
target: this.pickerElem,
|
||||
content: dropContentElem,
|
||||
position: 'bottom center',
|
||||
classes: 'drop-popover',
|
||||
openOn: 'hover',
|
||||
hoverCloseDelay: 200,
|
||||
remove: true,
|
||||
tetherOptions: {
|
||||
constraints: [{ to: 'scrollParent', attachment: 'none both' }],
|
||||
attachment: 'bottom center',
|
||||
},
|
||||
});
|
||||
|
||||
drop.on('close', this.closeColorPicker.bind(this));
|
||||
|
||||
this.colorPickerDrop = drop;
|
||||
this.colorPickerDrop.open();
|
||||
};
|
||||
|
||||
closeColorPicker() {
|
||||
setTimeout(() => {
|
||||
this.destroyDrop();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
destroyDrop() {
|
||||
if (this.colorPickerDrop && this.colorPickerDrop.tether) {
|
||||
this.colorPickerDrop.destroy();
|
||||
this.colorPickerDrop = null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { optionalClass, children } = this.props;
|
||||
return (
|
||||
<div className={optionalClass} ref={e => (this.pickerElem = e)} onClick={this.onClickToOpen}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||
|
||||
export interface SeriesColorPickerPopoverProps {
|
||||
color: string;
|
||||
yaxis?: number;
|
||||
onColorChange: (color: string) => void;
|
||||
onToggleAxis?: () => void;
|
||||
}
|
||||
|
||||
export class SeriesColorPickerPopover extends React.PureComponent<SeriesColorPickerPopoverProps, any> {
|
||||
render() {
|
||||
return (
|
||||
<div className="graph-legend-popover">
|
||||
{this.props.yaxis && <AxisSelector yaxis={this.props.yaxis} onToggleAxis={this.props.onToggleAxis} />}
|
||||
<ColorPickerPopover color={this.props.color} onColorSelect={this.props.onColorChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface AxisSelectorProps {
|
||||
yaxis: number;
|
||||
onToggleAxis?: () => void;
|
||||
}
|
||||
|
||||
interface AxisSelectorState {
|
||||
yaxis: number;
|
||||
}
|
||||
|
||||
export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
|
||||
constructor(props: AxisSelectorProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
yaxis: this.props.yaxis,
|
||||
};
|
||||
this.onToggleAxis = this.onToggleAxis.bind(this);
|
||||
}
|
||||
|
||||
onToggleAxis() {
|
||||
this.setState({
|
||||
yaxis: this.state.yaxis === 2 ? 1 : 2,
|
||||
});
|
||||
|
||||
if (this.props.onToggleAxis) {
|
||||
this.props.onToggleAxis();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const leftButtonClass = this.state.yaxis === 1 ? 'btn-success' : 'btn-inverse';
|
||||
const rightButtonClass = this.state.yaxis === 2 ? 'btn-success' : 'btn-inverse';
|
||||
|
||||
return (
|
||||
<div className="p-b-1">
|
||||
<label className="small p-r-1">Y Axis:</label>
|
||||
<button onClick={this.onToggleAxis} className={'btn btn-small ' + leftButtonClass}>
|
||||
Left
|
||||
</button>
|
||||
<button onClick={this.onToggleAxis} className={'btn btn-small ' + rightButtonClass}>
|
||||
Right
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import 'vendor/spectrum';
|
||||
|
||||
export interface Props {
|
||||
color: string;
|
||||
options: object;
|
||||
onColorSelect: (c: string) => void;
|
||||
}
|
||||
|
||||
export class SpectrumPicker extends React.Component<Props, any> {
|
||||
elem: any;
|
||||
isMoving: boolean;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.onSpectrumMove = this.onSpectrumMove.bind(this);
|
||||
this.setComponentElem = this.setComponentElem.bind(this);
|
||||
}
|
||||
|
||||
setComponentElem(elem: any) {
|
||||
this.elem = $(elem);
|
||||
}
|
||||
|
||||
onSpectrumMove(color: any) {
|
||||
this.isMoving = true;
|
||||
this.props.onColorSelect(color);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const spectrumOptions = _.assignIn(
|
||||
{
|
||||
flat: true,
|
||||
showAlpha: true,
|
||||
showButtons: false,
|
||||
color: this.props.color,
|
||||
appendTo: this.elem,
|
||||
move: this.onSpectrumMove,
|
||||
},
|
||||
this.props.options
|
||||
);
|
||||
|
||||
this.elem.spectrum(spectrumOptions);
|
||||
this.elem.spectrum('show');
|
||||
this.elem.spectrum('set', this.props.color);
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps: any) {
|
||||
// If user move pointer over spectrum field this produce 'move' event and component
|
||||
// may update props.color. We don't want to update spectrum color in this case, so we can use
|
||||
// isMoving flag for tracking moving state. Flag should be cleared in componentDidUpdate() which
|
||||
// is called after updating occurs (when user finished moving).
|
||||
if (!this.isMoving) {
|
||||
this.elem.spectrum('set', nextProps.color);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.isMoving) {
|
||||
this.isMoving = false;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.elem.spectrum('destroy');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="spectrum-container" ref={this.setComponentElem} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CollorPalette renders correctly 1`] = `
|
||||
<div
|
||||
className="graph-legend-popover"
|
||||
>
|
||||
<p
|
||||
className="m-b-0"
|
||||
>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#890f02",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#58140c",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#99440a",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#c15c17",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#967302",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#cca300",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3f6833",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#2f575e",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#64b0c8",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#052b51",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#0a50a1",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#584477",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3f2b5b",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#511749",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#e24d42",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#bf1b00",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#ef843c",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#f4d598",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#e5ac0e",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#9ac48a",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#508642",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#6ed0e0",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#65c5db",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#0a437c",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#447ebc",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#614d93",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#d683ce",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#6d1f62",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#ea6460",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#e0752d",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#f9934e",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#fceaca",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle-o"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#eab839",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#b7dbab",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#629e51",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#70dbed",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#82b5d8",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#1f78c1",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#aea2e0",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#705da0",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#e5a8e2",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#962d82",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#f29191",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#fce2de",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#f9ba8f",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#f9e2d2",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#f2c96d",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#e0f9d7",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#7eb26d",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#cffaff",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#badff4",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#5195ce",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#dedaf7",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#806eb7",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#f9d9f9",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
<i
|
||||
className="pointer fa fa-circle"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "#ba43a9",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
</i>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import CustomScrollbar from './CustomScrollbar';
|
||||
|
||||
describe('CustomScrollbar', () => {
|
||||
it('renders correctly', () => {
|
||||
const tree = renderer
|
||||
.create(
|
||||
<CustomScrollbar>
|
||||
<p>Scrollable content</p>
|
||||
</CustomScrollbar>
|
||||
)
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import Scrollbars from 'react-custom-scrollbars';
|
||||
|
||||
interface Props {
|
||||
customClassName?: string;
|
||||
autoHide?: boolean;
|
||||
autoHideTimeout?: number;
|
||||
autoHideDuration?: number;
|
||||
hideTracksWhenNotNeeded?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps component into <Scrollbars> component from `react-custom-scrollbars`
|
||||
*/
|
||||
export class CustomScrollbar extends PureComponent<Props> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
customClassName: 'custom-scrollbars',
|
||||
autoHide: true,
|
||||
autoHideTimeout: 200,
|
||||
autoHideDuration: 200,
|
||||
hideTracksWhenNotNeeded: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { customClassName, children, ...scrollProps } = this.props;
|
||||
|
||||
return (
|
||||
<Scrollbars
|
||||
className={customClassName}
|
||||
autoHeight={true}
|
||||
// These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
|
||||
// Before these where set to inhert but that caused problems with cut of legends in firefox
|
||||
autoHeightMin={'0'}
|
||||
autoHeightMax={'100%'}
|
||||
renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
|
||||
renderTrackVertical={props => <div {...props} className="track-vertical" />}
|
||||
renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
|
||||
renderThumbVertical={props => <div {...props} className="thumb-vertical" />}
|
||||
renderView={props => <div {...props} className="view" />}
|
||||
{...scrollProps}
|
||||
>
|
||||
{children}
|
||||
</Scrollbars>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomScrollbar;
|
||||
@@ -0,0 +1,40 @@
|
||||
.custom-scrollbars {
|
||||
// Fix for Firefox. For some reason sometimes .view container gets a height of its content, but in order to
|
||||
// make scroll working it should fit outer container size (scroll appears only when inner container size is
|
||||
// greater than outer one).
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
.view {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.track-vertical {
|
||||
border-radius: 3px;
|
||||
width: 6px !important;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.track-horizontal {
|
||||
border-radius: 3px;
|
||||
height: 6px !important;
|
||||
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
.thumb-vertical {
|
||||
@include gradient-vertical($scrollbarBackground, $scrollbarBackground2);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.thumb-horizontal {
|
||||
@include gradient-horizontal($scrollbarBackground, $scrollbarBackground2);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CustomScrollbar renders correctly 1`] = `
|
||||
<div
|
||||
className="custom-scrollbars"
|
||||
style={
|
||||
Object {
|
||||
"height": "auto",
|
||||
"maxHeight": "100%",
|
||||
"minHeight": "0",
|
||||
"overflow": "hidden",
|
||||
"position": "relative",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="view"
|
||||
style={
|
||||
Object {
|
||||
"WebkitOverflowScrolling": "touch",
|
||||
"bottom": undefined,
|
||||
"left": undefined,
|
||||
"marginBottom": 0,
|
||||
"marginRight": 0,
|
||||
"maxHeight": "calc(100% + 0px)",
|
||||
"minHeight": "calc(0 + 0px)",
|
||||
"overflow": "scroll",
|
||||
"position": "relative",
|
||||
"right": undefined,
|
||||
"top": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<p>
|
||||
Scrollable content
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="track-horizontal"
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
"height": 6,
|
||||
"opacity": 0,
|
||||
"position": "absolute",
|
||||
"transition": "opacity 200ms",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="thumb-horizontal"
|
||||
style={
|
||||
Object {
|
||||
"display": "block",
|
||||
"height": "100%",
|
||||
"position": "relative",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="track-vertical"
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
"opacity": 0,
|
||||
"position": "absolute",
|
||||
"transition": "opacity 200ms",
|
||||
"width": 6,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="thumb-vertical"
|
||||
style={
|
||||
Object {
|
||||
"display": "block",
|
||||
"position": "relative",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,11 @@
|
||||
import React, { SFC } from 'react';
|
||||
|
||||
interface LoadingPlaceholderProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const LoadingPlaceholder: SFC<LoadingPlaceholderProps> = ({ text }) => (
|
||||
<div className="gf-form-group">
|
||||
{text} <i className="fa fa-spinner fa-spin" />
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
import React, { SFC } from 'react';
|
||||
|
||||
interface Props {
|
||||
cols?: number;
|
||||
children: JSX.Element[] | JSX.Element;
|
||||
}
|
||||
|
||||
export const PanelOptionsGrid: SFC<Props> = ({ children }) => {
|
||||
|
||||
return (
|
||||
<div className="panel-options-grid">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
.panel-options-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
grid-row-gap: 10px;
|
||||
grid-column-gap: 10px;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Libraries
|
||||
import React, { SFC } from 'react';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
onClose?: () => void;
|
||||
children: JSX.Element | JSX.Element[];
|
||||
}
|
||||
|
||||
export const PanelOptionsGroup: SFC<Props> = props => {
|
||||
return (
|
||||
<div className="panel-options-group">
|
||||
{props.title && (
|
||||
<div className="panel-options-group__header">
|
||||
{props.title}
|
||||
{props.onClose && (
|
||||
<button className="btn btn-link" onClick={props.onClose}>
|
||||
<i className="fa fa-remove" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="panel-options-group__body">{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
.panel-options-group {
|
||||
margin-bottom: 10px;
|
||||
border: $panel-options-group-border;
|
||||
border-radius: $border-radius;
|
||||
background: $page-bg;
|
||||
}
|
||||
|
||||
.panel-options-group__header {
|
||||
padding: 4px 20px;
|
||||
font-size: 1.1rem;
|
||||
background: $panel-options-group-header-bg;
|
||||
position: relative;
|
||||
|
||||
.btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-options-group__body {
|
||||
padding: 20px;
|
||||
|
||||
&--queries {
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
35
packages/grafana-ui/src/components/Portal/Portal.tsx
Normal file
35
packages/grafana-ui/src/components/Portal/Portal.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { PureComponent } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
root?: HTMLElement;
|
||||
}
|
||||
|
||||
export class Portal extends PureComponent<Props> {
|
||||
node: HTMLElement = document.createElement('div');
|
||||
portalRoot: HTMLElement;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const {
|
||||
className,
|
||||
root = document.body
|
||||
} = this.props;
|
||||
|
||||
if (className) {
|
||||
this.node.classList.add(className);
|
||||
}
|
||||
|
||||
this.portalRoot = root;
|
||||
this.portalRoot.appendChild(this.node);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.portalRoot.removeChild(this.node);
|
||||
}
|
||||
|
||||
render() {
|
||||
return ReactDOM.createPortal(this.props.children, this.node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
||||
// @ts-ignore
|
||||
import { components } from '@torkelo/react-select';
|
||||
|
||||
export const IndicatorsContainer = (props: any) => {
|
||||
const isOpen = props.selectProps.menuIsOpen;
|
||||
return (
|
||||
<components.IndicatorsContainer {...props}>
|
||||
<span
|
||||
className={`gf-form-select-box__select-arrow ${isOpen ? `gf-form-select-box__select-arrow--reversed` : ''}`}
|
||||
/>
|
||||
</components.IndicatorsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndicatorsContainer;
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
||||
// @ts-ignore
|
||||
import { components } from '@torkelo/react-select';
|
||||
// @ts-ignore
|
||||
import { OptionProps } from '@torkelo/react-select/lib/components/Option';
|
||||
|
||||
export interface Props {
|
||||
children: Element;
|
||||
}
|
||||
|
||||
export const NoOptionsMessage = (props: OptionProps<any>) => {
|
||||
const { children } = props;
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
<div className="gf-form-select-box__desc-option">
|
||||
<div className="gf-form-select-box__desc-option__body">{children}</div>
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoOptionsMessage;
|
||||
237
packages/grafana-ui/src/components/Select/Select.tsx
Normal file
237
packages/grafana-ui/src/components/Select/Select.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
// Libraries
|
||||
import classNames from 'classnames';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
||||
// @ts-ignore
|
||||
import { default as ReactSelect } from '@torkelo/react-select';
|
||||
// @ts-ignore
|
||||
import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
|
||||
// @ts-ignore
|
||||
import { components } from '@torkelo/react-select';
|
||||
|
||||
// Components
|
||||
import { SelectOption, SingleValue } from './SelectOption';
|
||||
import SelectOptionGroup from './SelectOptionGroup';
|
||||
import IndicatorsContainer from './IndicatorsContainer';
|
||||
import NoOptionsMessage from './NoOptionsMessage';
|
||||
import resetSelectStyles from './resetSelectStyles';
|
||||
import { CustomScrollbar } from '@grafana/ui';
|
||||
|
||||
export interface SelectOptionItem {
|
||||
label?: string;
|
||||
value?: any;
|
||||
imgUrl?: string;
|
||||
description?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface CommonProps {
|
||||
defaultValue?: any;
|
||||
getOptionLabel?: (item: SelectOptionItem) => string;
|
||||
getOptionValue?: (item: SelectOptionItem) => string;
|
||||
onChange: (item: SelectOptionItem) => {} | void;
|
||||
placeholder?: string;
|
||||
width?: number;
|
||||
value?: SelectOptionItem;
|
||||
className?: string;
|
||||
isDisabled?: boolean;
|
||||
isSearchable?: boolean;
|
||||
isClearable?: boolean;
|
||||
autoFocus?: boolean;
|
||||
openMenuOnFocus?: boolean;
|
||||
onBlur?: () => void;
|
||||
maxMenuHeight?: number;
|
||||
isLoading: boolean;
|
||||
noOptionsMessage?: () => string;
|
||||
isMulti?: boolean;
|
||||
backspaceRemovesValue: boolean;
|
||||
}
|
||||
|
||||
interface SelectProps {
|
||||
options: SelectOptionItem[];
|
||||
}
|
||||
|
||||
interface AsyncProps {
|
||||
defaultOptions: boolean;
|
||||
loadOptions: (query: string) => Promise<SelectOptionItem[]>;
|
||||
loadingMessage?: () => string;
|
||||
}
|
||||
|
||||
export const MenuList = (props: any) => {
|
||||
return (
|
||||
<components.MenuList {...props}>
|
||||
<CustomScrollbar autoHide={false}>{props.children}</CustomScrollbar>
|
||||
</components.MenuList>
|
||||
);
|
||||
};
|
||||
|
||||
export class Select extends PureComponent<CommonProps & SelectProps> {
|
||||
static defaultProps = {
|
||||
width: null,
|
||||
className: '',
|
||||
isDisabled: false,
|
||||
isSearchable: true,
|
||||
isClearable: false,
|
||||
isMulti: false,
|
||||
openMenuOnFocus: false,
|
||||
autoFocus: false,
|
||||
isLoading: false,
|
||||
backspaceRemovesValue: true,
|
||||
maxMenuHeight: 300,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
defaultValue,
|
||||
getOptionLabel,
|
||||
getOptionValue,
|
||||
onChange,
|
||||
options,
|
||||
placeholder,
|
||||
width,
|
||||
value,
|
||||
className,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isSearchable,
|
||||
isClearable,
|
||||
backspaceRemovesValue,
|
||||
isMulti,
|
||||
autoFocus,
|
||||
openMenuOnFocus,
|
||||
onBlur,
|
||||
maxMenuHeight,
|
||||
noOptionsMessage,
|
||||
} = this.props;
|
||||
|
||||
let widthClass = '';
|
||||
if (width) {
|
||||
widthClass = 'width-' + width;
|
||||
}
|
||||
|
||||
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
|
||||
|
||||
return (
|
||||
<ReactSelect
|
||||
classNamePrefix="gf-form-select-box"
|
||||
className={selectClassNames}
|
||||
components={{
|
||||
Option: SelectOption,
|
||||
SingleValue,
|
||||
IndicatorsContainer,
|
||||
MenuList,
|
||||
Group: SelectOptionGroup,
|
||||
}}
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
getOptionLabel={getOptionLabel}
|
||||
getOptionValue={getOptionValue}
|
||||
menuShouldScrollIntoView={false}
|
||||
isSearchable={isSearchable}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
placeholder={placeholder || 'Choose'}
|
||||
styles={resetSelectStyles()}
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
isClearable={isClearable}
|
||||
autoFocus={autoFocus}
|
||||
onBlur={onBlur}
|
||||
openMenuOnFocus={openMenuOnFocus}
|
||||
maxMenuHeight={maxMenuHeight}
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
isMulti={isMulti}
|
||||
backspaceRemovesValue={backspaceRemovesValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
|
||||
static defaultProps = {
|
||||
width: null,
|
||||
className: '',
|
||||
components: {},
|
||||
loadingMessage: () => 'Loading...',
|
||||
isDisabled: false,
|
||||
isClearable: false,
|
||||
isMulti: false,
|
||||
isSearchable: true,
|
||||
backspaceRemovesValue: true,
|
||||
autoFocus: false,
|
||||
openMenuOnFocus: false,
|
||||
maxMenuHeight: 300,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
defaultValue,
|
||||
getOptionLabel,
|
||||
getOptionValue,
|
||||
onChange,
|
||||
placeholder,
|
||||
width,
|
||||
value,
|
||||
className,
|
||||
loadOptions,
|
||||
defaultOptions,
|
||||
isLoading,
|
||||
loadingMessage,
|
||||
noOptionsMessage,
|
||||
isDisabled,
|
||||
isSearchable,
|
||||
isClearable,
|
||||
backspaceRemovesValue,
|
||||
autoFocus,
|
||||
onBlur,
|
||||
openMenuOnFocus,
|
||||
maxMenuHeight,
|
||||
isMulti,
|
||||
} = this.props;
|
||||
|
||||
let widthClass = '';
|
||||
if (width) {
|
||||
widthClass = 'width-' + width;
|
||||
}
|
||||
|
||||
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
|
||||
|
||||
return (
|
||||
<ReactAsyncSelect
|
||||
classNamePrefix="gf-form-select-box"
|
||||
className={selectClassNames}
|
||||
components={{
|
||||
Option,
|
||||
SingleValue,
|
||||
IndicatorsContainer,
|
||||
NoOptionsMessage,
|
||||
}}
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
getOptionLabel={getOptionLabel}
|
||||
getOptionValue={getOptionValue}
|
||||
menuShouldScrollIntoView={false}
|
||||
onChange={onChange}
|
||||
loadOptions={loadOptions}
|
||||
isLoading={isLoading}
|
||||
defaultOptions={defaultOptions}
|
||||
placeholder={placeholder || 'Choose'}
|
||||
styles={resetSelectStyles()}
|
||||
loadingMessage={loadingMessage}
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
isDisabled={isDisabled}
|
||||
isSearchable={isSearchable}
|
||||
isClearable={isClearable}
|
||||
autoFocus={autoFocus}
|
||||
onBlur={onBlur}
|
||||
openMenuOnFocus={openMenuOnFocus}
|
||||
maxMenuHeight={maxMenuHeight}
|
||||
isMulti={isMulti}
|
||||
backspaceRemovesValue={backspaceRemovesValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Select;
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import SelectOption from './SelectOption';
|
||||
import { OptionProps } from 'react-select/lib/components/Option';
|
||||
|
||||
// @ts-ignore
|
||||
const model: OptionProps<any> = {
|
||||
cx: jest.fn(),
|
||||
clearValue: jest.fn(),
|
||||
getStyles: jest.fn(),
|
||||
getValue: jest.fn(),
|
||||
hasValue: true,
|
||||
isMulti: false,
|
||||
options: [],
|
||||
selectOption: jest.fn(),
|
||||
selectProps: {},
|
||||
setValue: jest.fn(),
|
||||
isDisabled: false,
|
||||
isFocused: false,
|
||||
isSelected: false,
|
||||
innerRef: null,
|
||||
innerProps: {
|
||||
id: '',
|
||||
key: '',
|
||||
onClick: jest.fn(),
|
||||
onMouseOver: jest.fn(),
|
||||
tabIndex: 1,
|
||||
},
|
||||
label: 'Option label',
|
||||
type: 'option',
|
||||
children: 'Model title',
|
||||
className: 'class-for-user-picker',
|
||||
};
|
||||
|
||||
describe('SelectOption', () => {
|
||||
it('renders correctly', () => {
|
||||
const tree = renderer
|
||||
.create(
|
||||
<SelectOption
|
||||
{...model}
|
||||
data={{
|
||||
imgUrl: 'url/to/avatar',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
47
packages/grafana-ui/src/components/Select/SelectOption.tsx
Normal file
47
packages/grafana-ui/src/components/Select/SelectOption.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
||||
// @ts-ignore
|
||||
import { components } from '@torkelo/react-select';
|
||||
import { OptionProps } from 'react-select/lib/components/Option';
|
||||
|
||||
// https://github.com/JedWatson/react-select/issues/3038
|
||||
interface ExtendedOptionProps extends OptionProps<any> {
|
||||
data: {
|
||||
description?: string;
|
||||
imgUrl?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const SelectOption = (props: ExtendedOptionProps) => {
|
||||
const { children, isSelected, data } = props;
|
||||
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
<div className="gf-form-select-box__desc-option">
|
||||
{data.imgUrl && <img className="gf-form-select-box__desc-option__img" src={data.imgUrl} />}
|
||||
<div className="gf-form-select-box__desc-option__body">
|
||||
<div>{children}</div>
|
||||
{data.description && <div className="gf-form-select-box__desc-option__desc">{data.description}</div>}
|
||||
</div>
|
||||
{isSelected && <i className="fa fa-check" aria-hidden="true" />}
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
|
||||
// was not able to type this without typescript error
|
||||
export const SingleValue = (props: any) => {
|
||||
const { children, data } = props;
|
||||
|
||||
return (
|
||||
<components.SingleValue {...props}>
|
||||
<div className="gf-form-select-box__img-value">
|
||||
{data.imgUrl && <img className="gf-form-select-box__desc-option__img" src={data.imgUrl} />}
|
||||
{children}
|
||||
</div>
|
||||
</components.SingleValue>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectOption;
|
||||
@@ -0,0 +1,53 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { GroupProps } from 'react-select/lib/components/Group';
|
||||
|
||||
interface ExtendedGroupProps extends GroupProps<any> {
|
||||
data: any;
|
||||
}
|
||||
|
||||
interface State {
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
export default class SelectOptionGroup extends PureComponent<ExtendedGroupProps, State> {
|
||||
state = {
|
||||
expanded: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.selectProps) {
|
||||
const value = this.props.selectProps.value[this.props.selectProps.value.length - 1];
|
||||
|
||||
if (value && this.props.options.some(option => option.value === value)) {
|
||||
this.setState({ expanded: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(nextProps: ExtendedGroupProps) {
|
||||
if (nextProps.selectProps.inputValue !== '') {
|
||||
this.setState({ expanded: true });
|
||||
}
|
||||
}
|
||||
|
||||
onToggleChildren = () => {
|
||||
this.setState(prevState => ({
|
||||
expanded: !prevState.expanded,
|
||||
}));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { children, label } = this.props;
|
||||
const { expanded } = this.state;
|
||||
|
||||
return (
|
||||
<div className="gf-form-select-box__option-group">
|
||||
<div className="gf-form-select-box__option-group__header" onClick={this.onToggleChildren}>
|
||||
<span className="flex-grow">{label}</span>
|
||||
<i className={`fa ${expanded ? 'fa-caret-left' : 'fa-caret-down'}`} />{' '}
|
||||
</div>
|
||||
{expanded && children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
189
packages/grafana-ui/src/components/Select/_Select.scss
Normal file
189
packages/grafana-ui/src/components/Select/_Select.scss
Normal file
@@ -0,0 +1,189 @@
|
||||
$select-input-height: 35px;
|
||||
$select-input-bg-disabled: $input-bg-disabled;
|
||||
|
||||
@mixin select-control() {
|
||||
width: 100%;
|
||||
margin-right: $gf-form-margin;
|
||||
@include border-radius($input-border-radius-sm);
|
||||
background-color: $input-bg;
|
||||
}
|
||||
|
||||
@mixin select-control-focus() {
|
||||
border-color: $input-border-focus;
|
||||
outline: none;
|
||||
$shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px $input-box-shadow-focus;
|
||||
@include box-shadow($shadow);
|
||||
}
|
||||
|
||||
.gf-form-select-box__control {
|
||||
@include select-control();
|
||||
border: 1px solid $input-border-color;
|
||||
color: $input-color;
|
||||
cursor: default;
|
||||
display: table;
|
||||
border-spacing: 0;
|
||||
border-collapse: separate;
|
||||
height: $select-input-height;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gf-form-select-box__control--is-focused {
|
||||
background-color: $input-bg;
|
||||
@include select-control-focus();
|
||||
}
|
||||
|
||||
.gf-form-select-box__control--is-disabled {
|
||||
background-color: $select-input-bg-disabled;
|
||||
}
|
||||
|
||||
.gf-form-select-box__control--menu-right {
|
||||
.gf-form-select-box__menu {
|
||||
right: 0;
|
||||
left: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-select-box__input {
|
||||
padding-left: 5px;
|
||||
input {
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-select-box__menu {
|
||||
background: $input-bg;
|
||||
box-shadow: $menu-dropdown-shadow;
|
||||
position: absolute;
|
||||
z-index: $zindex-dropdown;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.gf-form-select-box__menu-list {
|
||||
overflow-y: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.tag-filter .gf-form-select-box__menu {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* .gf-form-select-box__single-value { */
|
||||
/* } */
|
||||
|
||||
.gf-form-select-box__multi-value {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.gf-form-select-box__option {
|
||||
border-left: 2px solid transparent;
|
||||
white-space: nowrap;
|
||||
background-color: $input-bg;
|
||||
|
||||
&.gf-form-select-box__option--is-focused {
|
||||
color: $dropdownLinkColorHover;
|
||||
background: $menu-dropdown-hover-bg;
|
||||
@include left-brand-border-gradient();
|
||||
}
|
||||
|
||||
&.gf-form-select-box__option--is-selected {
|
||||
.fa {
|
||||
color: $input-color-select-arrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-select-box__control--is-focused .gf-form-select-box__placeholder {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gf-form-select-box__value-container {
|
||||
display: table-cell;
|
||||
padding: 6px 10px;
|
||||
> div {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-select-box__indicators {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.gf-form-select-box__select-arrow {
|
||||
border-color: $input-color-select-arrow transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 4px 4px 2.5px;
|
||||
display: inline-block;
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: relative;
|
||||
|
||||
&.gf-form-select-box__select-arrow--reversed {
|
||||
border-color: transparent transparent $input-color-select-arrow;
|
||||
top: -2px;
|
||||
border-width: 0 4px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-input--form-dropdown {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gf-form--has-input-icon {
|
||||
.gf-form-select-box__value-container {
|
||||
padding-left: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-select-box__desc-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
justify-items: center;
|
||||
cursor: pointer;
|
||||
padding: 7px 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gf-form-select-box__desc-option__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
padding-right: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.gf-form-select-box__desc-option__desc {
|
||||
font-weight: normal;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.gf-form-select-box__desc-option__img {
|
||||
width: 16px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.gf-form-select-box__option-group__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
justify-items: center;
|
||||
cursor: pointer;
|
||||
padding: 7px 10px;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid $hr-border-color;
|
||||
text-transform: capitalize;
|
||||
|
||||
.fa {
|
||||
padding-right: 2px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SelectOption renders correctly 1`] = `
|
||||
<div
|
||||
id=""
|
||||
onClick={[MockFunction]}
|
||||
onMouseOver={[MockFunction]}
|
||||
tabIndex={1}
|
||||
>
|
||||
<div
|
||||
className="gf-form-select-box__desc-option"
|
||||
>
|
||||
<img
|
||||
className="gf-form-select-box__desc-option__img"
|
||||
src="url/to/avatar"
|
||||
/>
|
||||
<div
|
||||
className="gf-form-select-box__desc-option__body"
|
||||
>
|
||||
<div>
|
||||
Model title
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,27 @@
|
||||
export default function resetSelectStyles() {
|
||||
return {
|
||||
clearIndicator: () => ({}),
|
||||
container: () => ({}),
|
||||
control: () => ({}),
|
||||
dropdownIndicator: () => ({}),
|
||||
group: () => ({}),
|
||||
groupHeading: () => ({}),
|
||||
indicatorsContainer: () => ({}),
|
||||
indicatorSeparator: () => ({}),
|
||||
input: () => ({}),
|
||||
loadingIndicator: () => ({}),
|
||||
loadingMessage: () => ({}),
|
||||
menu: () => ({}),
|
||||
menuList: ({ maxHeight }: { maxHeight: number }) => ({
|
||||
maxHeight,
|
||||
}),
|
||||
multiValue: () => ({}),
|
||||
multiValueLabel: () => ({}),
|
||||
multiValueRemove: () => ({}),
|
||||
noOptionsMessage: () => ({}),
|
||||
option: () => ({}),
|
||||
placeholder: () => ({}),
|
||||
singleValue: () => ({}),
|
||||
valueContainer: () => ({}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { ThresholdsEditor, Props } from './ThresholdsEditor';
|
||||
import { BasicGaugeColor } from '../../types';
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
onChange: jest.fn(),
|
||||
thresholds: [],
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
return shallow(<ThresholdsEditor {...props} />).instance() as ThresholdsEditor;
|
||||
};
|
||||
|
||||
describe('Add threshold', () => {
|
||||
it('should add threshold', () => {
|
||||
const instance = setup();
|
||||
|
||||
instance.onAddThreshold(0);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }]);
|
||||
});
|
||||
|
||||
it('should add another threshold above a first', () => {
|
||||
const instance = setup({
|
||||
thresholds: [{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }],
|
||||
});
|
||||
|
||||
instance.onAddThreshold(1);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 1, value: 75, color: 'rgb(170, 95, 61)' },
|
||||
{ index: 0, value: 50, color: 'rgb(127, 115, 64)' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change threshold value', () => {
|
||||
it('should update value and resort rows', () => {
|
||||
const instance = setup();
|
||||
const mockThresholds = [
|
||||
{ index: 0, value: 50, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
];
|
||||
|
||||
instance.state = {
|
||||
baseColor: BasicGaugeColor.Green,
|
||||
thresholds: mockThresholds,
|
||||
};
|
||||
|
||||
const mockEvent = { target: { value: 78 } };
|
||||
|
||||
instance.onChangeThresholdValue(mockEvent, mockThresholds[0]);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 0, value: 78, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
{ index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import tinycolor, { ColorInput } from 'tinycolor2';
|
||||
|
||||
import { Threshold, BasicGaugeColor } from '../../types';
|
||||
import { ColorPicker } from '../ColorPicker/ColorPicker';
|
||||
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
|
||||
|
||||
export interface Props {
|
||||
thresholds: Threshold[];
|
||||
onChange: (thresholds: Threshold[]) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
thresholds: Threshold[];
|
||||
baseColor: string;
|
||||
}
|
||||
|
||||
export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = { thresholds: props.thresholds, baseColor: BasicGaugeColor.Green };
|
||||
}
|
||||
|
||||
onAddThreshold = (index: number) => {
|
||||
const maxValue = 100; // hardcoded for now before we add the base threshold
|
||||
const minValue = 0; // hardcoded for now before we add the base threshold
|
||||
const { thresholds } = this.state;
|
||||
|
||||
const newThresholds = thresholds.map(threshold => {
|
||||
if (threshold.index >= index) {
|
||||
threshold = {
|
||||
...threshold,
|
||||
index: threshold.index + 1,
|
||||
};
|
||||
}
|
||||
|
||||
return threshold;
|
||||
});
|
||||
|
||||
// Setting value to a value between the previous thresholds
|
||||
let value;
|
||||
|
||||
if (index === 0 && thresholds.length === 0) {
|
||||
value = maxValue - (maxValue - minValue) / 2;
|
||||
} else if (index === 0 && thresholds.length > 0) {
|
||||
value = newThresholds[index + 1].value - (newThresholds[index + 1].value - minValue) / 2;
|
||||
} else if (index > newThresholds[newThresholds.length - 1].index) {
|
||||
value = maxValue - (maxValue - newThresholds[index - 1].value) / 2;
|
||||
}
|
||||
|
||||
// Set a color that lies between the previous thresholds
|
||||
let color;
|
||||
if (index === 0 && thresholds.length === 0) {
|
||||
color = tinycolor.mix(BasicGaugeColor.Green, BasicGaugeColor.Red, 50).toRgbString();
|
||||
} else {
|
||||
color = tinycolor.mix(thresholds[index - 1].color as ColorInput, BasicGaugeColor.Red, 50).toRgbString();
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
thresholds: this.sortThresholds([
|
||||
...newThresholds,
|
||||
{
|
||||
index,
|
||||
value: value as number,
|
||||
color,
|
||||
},
|
||||
]),
|
||||
},
|
||||
() => this.updateGauge()
|
||||
);
|
||||
};
|
||||
|
||||
onRemoveThreshold = (threshold: Threshold) => {
|
||||
this.setState(
|
||||
prevState => ({ thresholds: prevState.thresholds.filter(t => t !== threshold) }),
|
||||
() => this.updateGauge()
|
||||
);
|
||||
};
|
||||
|
||||
onChangeThresholdValue = (event: any, threshold: Threshold) => {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
const newThresholds = thresholds.map(t => {
|
||||
if (t === threshold) {
|
||||
t = { ...t, value: event.target.value };
|
||||
}
|
||||
|
||||
return t;
|
||||
});
|
||||
|
||||
this.setState({ thresholds: newThresholds });
|
||||
};
|
||||
|
||||
onChangeThresholdColor = (threshold: Threshold, color: string) => {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
const newThresholds = thresholds.map(t => {
|
||||
if (t === threshold) {
|
||||
t = { ...t, color: color };
|
||||
}
|
||||
|
||||
return t;
|
||||
});
|
||||
|
||||
this.setState(
|
||||
{
|
||||
thresholds: newThresholds,
|
||||
},
|
||||
() => this.updateGauge()
|
||||
);
|
||||
};
|
||||
|
||||
onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds);
|
||||
onBlur = () => {
|
||||
this.setState(prevState => ({ thresholds: this.sortThresholds(prevState.thresholds) }));
|
||||
|
||||
this.updateGauge();
|
||||
};
|
||||
|
||||
updateGauge = () => {
|
||||
this.props.onChange(this.state.thresholds);
|
||||
};
|
||||
|
||||
sortThresholds = (thresholds: Threshold[]) => {
|
||||
return thresholds.sort((t1, t2) => {
|
||||
return t2.value - t1.value;
|
||||
});
|
||||
};
|
||||
|
||||
renderThresholds() {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
return thresholds.map((threshold, index) => {
|
||||
return (
|
||||
<div className="threshold-row" key={`${threshold.index}-${index}`}>
|
||||
<div className="threshold-row-inner">
|
||||
<div className="threshold-row-color">
|
||||
{threshold.color && (
|
||||
<div className="threshold-row-color-inner">
|
||||
<ColorPicker
|
||||
color={threshold.color}
|
||||
onChange={color => this.onChangeThresholdColor(threshold, color)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
className="threshold-row-input"
|
||||
type="text"
|
||||
onChange={event => this.onChangeThresholdValue(event, threshold)}
|
||||
value={threshold.value}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
<div onClick={() => this.onRemoveThreshold(threshold)} className="threshold-row-remove">
|
||||
<i className="fa fa-times" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderIndicator() {
|
||||
const { thresholds } = this.state;
|
||||
|
||||
return thresholds.map((t, i) => {
|
||||
return (
|
||||
<div key={`${t.value}-${i}`} className="indicator-section">
|
||||
<div onClick={() => this.onAddThreshold(t.index + 1)} style={{ height: '50%', backgroundColor: t.color }} />
|
||||
<div onClick={() => this.onAddThreshold(t.index)} style={{ height: '50%', backgroundColor: t.color }} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderBaseIndicator() {
|
||||
return (
|
||||
<div className="indicator-section" style={{ height: '100%' }}>
|
||||
<div
|
||||
onClick={() => this.onAddThreshold(0)}
|
||||
style={{ height: '100%', backgroundColor: BasicGaugeColor.Green }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderBase() {
|
||||
const baseColor = BasicGaugeColor.Green;
|
||||
|
||||
return (
|
||||
<div className="threshold-row threshold-row-base">
|
||||
<div className="threshold-row-inner threshold-row-inner--base">
|
||||
<div className="threshold-row-color">
|
||||
<div className="threshold-row-color-inner">
|
||||
<ColorPicker color={baseColor} onChange={color => this.onChangeBaseColor(color)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="threshold-row-label">Base</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PanelOptionsGroup title="Thresholds">
|
||||
<div className="thresholds">
|
||||
<div className="color-indicators">
|
||||
{this.renderIndicator()}
|
||||
{this.renderBaseIndicator()}
|
||||
</div>
|
||||
<div className="threshold-rows">
|
||||
{this.renderThresholds()}
|
||||
{this.renderBase()}
|
||||
</div>
|
||||
</div>
|
||||
</PanelOptionsGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
.thresholds {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.threshold-rows {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.threshold-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 3px;
|
||||
padding: 5px;
|
||||
|
||||
&::before {
|
||||
font-family: 'FontAwesome';
|
||||
content: '\f0d9';
|
||||
color: $input-label-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.threshold-row-inner {
|
||||
border: 1px solid $input-label-border-color;
|
||||
border-radius: $border-radius;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 37px;
|
||||
|
||||
&--base {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.threshold-row-color {
|
||||
width: 36px;
|
||||
border-right: 1px solid $input-label-border-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: $input-bg;
|
||||
}
|
||||
|
||||
.threshold-row-color-inner {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.threshold-row-input {
|
||||
padding: 8px 10px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.threshold-row-label {
|
||||
background-color: $input-label-bg;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.threshold-row-add-label {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.threshold-row-remove {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 37px;
|
||||
width: 37px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.threshold-row-add {
|
||||
border-right: $border-width solid $input-label-border-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
background-color: $green;
|
||||
}
|
||||
|
||||
.threshold-row-label {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.indicator-section {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-indicators {
|
||||
width: 15px;
|
||||
border-bottom-left-radius: $border-radius;
|
||||
border-bottom-right-radius: $border-radius;
|
||||
overflow: hidden;
|
||||
}
|
||||
77
packages/grafana-ui/src/components/Tooltip/Popper.tsx
Normal file
77
packages/grafana-ui/src/components/Tooltip/Popper.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import * as PopperJS from 'popper.js';
|
||||
import { Manager, Popper as ReactPopper } from 'react-popper';
|
||||
import { Portal } from '@grafana/ui';
|
||||
import Transition from 'react-transition-group/Transition';
|
||||
|
||||
export enum Themes {
|
||||
Default = 'popper__background--default',
|
||||
Error = 'popper__background--error',
|
||||
Brand = 'popper__background--brand',
|
||||
}
|
||||
|
||||
const defaultTransitionStyles = {
|
||||
transition: 'opacity 200ms linear',
|
||||
opacity: 0,
|
||||
};
|
||||
|
||||
const transitionStyles: {[key: string]: object} = {
|
||||
exited: { opacity: 0 },
|
||||
entering: { opacity: 0 },
|
||||
entered: { opacity: 1 },
|
||||
exiting: { opacity: 0 },
|
||||
};
|
||||
|
||||
interface Props extends React.DOMAttributes<HTMLDivElement> {
|
||||
renderContent: (content: any) => any;
|
||||
show: boolean;
|
||||
placement?: PopperJS.Placement;
|
||||
content: string | ((props: any) => JSX.Element);
|
||||
referenceElement: PopperJS.ReferenceObject;
|
||||
theme?: Themes;
|
||||
}
|
||||
|
||||
class Popper extends PureComponent<Props> {
|
||||
render() {
|
||||
const { renderContent, show, placement, onMouseEnter, onMouseLeave, theme } = this.props;
|
||||
const { content } = this.props;
|
||||
|
||||
const popperBackgroundClassName = 'popper__background' + (theme ? ' ' + theme : '');
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
|
||||
{transitionState => (
|
||||
<Portal>
|
||||
<ReactPopper placement={placement} referenceElement={this.props.referenceElement}>
|
||||
{({ ref, style, placement, arrowProps }) => {
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
ref={ref}
|
||||
style={{
|
||||
...style,
|
||||
...defaultTransitionStyles,
|
||||
...transitionStyles[transitionState],
|
||||
}}
|
||||
data-placement={placement}
|
||||
className="popper"
|
||||
>
|
||||
<div className={popperBackgroundClassName}>
|
||||
{renderContent(content)}
|
||||
<div ref={arrowProps.ref} data-placement={placement} className="popper__arrow" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ReactPopper>
|
||||
</Portal>
|
||||
)}
|
||||
</Transition>
|
||||
</Manager>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Popper;
|
||||
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import * as PopperJS from 'popper.js';
|
||||
import { Themes } from './Popper';
|
||||
|
||||
type PopperContent = string | (() => JSX.Element);
|
||||
|
||||
export interface UsingPopperProps {
|
||||
show?: boolean;
|
||||
placement?: PopperJS.Placement;
|
||||
content: PopperContent;
|
||||
children: JSX.Element;
|
||||
renderContent?: (content: PopperContent) => JSX.Element;
|
||||
theme?: Themes;
|
||||
}
|
||||
|
||||
type PopperControllerRenderProp = (
|
||||
showPopper: () => void,
|
||||
hidePopper: () => void,
|
||||
popperProps: {
|
||||
show: boolean;
|
||||
placement: PopperJS.Placement;
|
||||
content: string | ((props: any) => JSX.Element);
|
||||
renderContent: (content: any) => any;
|
||||
theme?: Themes;
|
||||
}
|
||||
) => JSX.Element;
|
||||
|
||||
interface Props {
|
||||
placement?: PopperJS.Placement;
|
||||
content: PopperContent;
|
||||
className?: string;
|
||||
children: PopperControllerRenderProp;
|
||||
theme?: Themes;
|
||||
}
|
||||
|
||||
interface State {
|
||||
placement: PopperJS.Placement;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
class PopperController extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
placement: this.props.placement || 'auto',
|
||||
show: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
if (nextProps.placement && nextProps.placement !== this.state.placement) {
|
||||
this.setState((prevState: State) => {
|
||||
return {
|
||||
...prevState,
|
||||
placement: nextProps.placement || 'auto',
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showPopper = () => {
|
||||
this.setState(prevState => ({
|
||||
...prevState,
|
||||
show: true,
|
||||
}));
|
||||
};
|
||||
|
||||
hidePopper = () => {
|
||||
this.setState(prevState => ({
|
||||
...prevState,
|
||||
show: false,
|
||||
}));
|
||||
};
|
||||
|
||||
renderContent(content: PopperContent) {
|
||||
if (typeof content === 'function') {
|
||||
// If it's a function we assume it's a React component
|
||||
const ReactComponent = content;
|
||||
return <ReactComponent />;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, content, theme } = this.props;
|
||||
const { show, placement } = this.state;
|
||||
|
||||
return children(this.showPopper, this.hidePopper, {
|
||||
show,
|
||||
placement,
|
||||
content,
|
||||
renderContent: this.renderContent,
|
||||
theme,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default PopperController;
|
||||
18
packages/grafana-ui/src/components/Tooltip/Tooltip.test.tsx
Normal file
18
packages/grafana-ui/src/components/Tooltip/Tooltip.test.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { Tooltip } from './Tooltip';
|
||||
|
||||
describe('Tooltip', () => {
|
||||
it('renders correctly', () => {
|
||||
const tree = renderer
|
||||
.create(
|
||||
<Tooltip placement="auto" content="Tooltip text">
|
||||
<a className="test-class" href="http://www.grafana.com">
|
||||
Link with tooltip
|
||||
</a>
|
||||
</Tooltip>
|
||||
)
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
32
packages/grafana-ui/src/components/Tooltip/Tooltip.tsx
Normal file
32
packages/grafana-ui/src/components/Tooltip/Tooltip.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React, { createRef } from 'react';
|
||||
import * as PopperJS from 'popper.js';
|
||||
import Popper from './Popper';
|
||||
import PopperController, { UsingPopperProps } from './PopperController';
|
||||
|
||||
export const Tooltip = ({ children, renderContent, ...controllerProps }: UsingPopperProps) => {
|
||||
const tooltipTriggerRef = createRef<PopperJS.ReferenceObject>();
|
||||
|
||||
return (
|
||||
<PopperController {...controllerProps}>
|
||||
{(showPopper, hidePopper, popperProps) => {
|
||||
return (
|
||||
<>
|
||||
{tooltipTriggerRef.current && (
|
||||
<Popper
|
||||
{...popperProps}
|
||||
onMouseEnter={showPopper}
|
||||
onMouseLeave={hidePopper}
|
||||
referenceElement={tooltipTriggerRef.current}
|
||||
/>
|
||||
)}
|
||||
{React.cloneElement(children, {
|
||||
ref: tooltipTriggerRef,
|
||||
onMouseEnter: showPopper,
|
||||
onMouseLeave: hidePopper,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</PopperController>
|
||||
);
|
||||
};
|
||||
132
packages/grafana-ui/src/components/Tooltip/_Tooltip.scss
Normal file
132
packages/grafana-ui/src/components/Tooltip/_Tooltip.scss
Normal file
@@ -0,0 +1,132 @@
|
||||
$popper-margin-from-ref: 5px;
|
||||
|
||||
|
||||
@mixin popper-theme($backgroundColor, $arrowColor) {
|
||||
background: $backgroundColor;
|
||||
.popper__arrow {
|
||||
border-color: $arrowColor;
|
||||
}
|
||||
}
|
||||
|
||||
.popper {
|
||||
position: absolute;
|
||||
z-index: $zindex-tooltip;
|
||||
color: $tooltipColor;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.popper__background {
|
||||
background: $tooltipBackground;
|
||||
border-radius: $border-radius;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
|
||||
padding: 10px;
|
||||
|
||||
// Themes
|
||||
&.popper__background--error {
|
||||
@include popper-theme($tooltipBackgroundError, $tooltipBackgroundError);
|
||||
}
|
||||
|
||||
&.popper__background--brand {
|
||||
@include popper-theme($tooltipBackgroundBrand, $tooltipBackgroundBrand);
|
||||
@include gradient-vertical($red, $orange);
|
||||
}
|
||||
}
|
||||
|
||||
.popper__arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
position: absolute;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.popper__arrow {
|
||||
border-color: $tooltipBackground;
|
||||
}
|
||||
|
||||
// Top
|
||||
.popper[data-placement^='top'] {
|
||||
padding-bottom: $popper-margin-from-ref;
|
||||
}
|
||||
.popper[data-placement^='top'] .popper__arrow {
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
bottom: -5px;
|
||||
left: calc(50% - 5px);
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
// Bottom
|
||||
.popper[data-placement^='bottom'] {
|
||||
padding-top: $popper-margin-from-ref;
|
||||
}
|
||||
.popper[data-placement^='bottom'] .popper__arrow {
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-top-color: transparent;
|
||||
top: 0;
|
||||
left: calc(50% - 5px);
|
||||
}
|
||||
|
||||
.popper[data-placement^='bottom-start'] {
|
||||
padding-top: $popper-margin-from-ref;
|
||||
}
|
||||
.popper[data-placement^='bottom-start'] .popper__arrow {
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-top-color: transparent;
|
||||
top: 0;
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
.popper[data-placement^='bottom-end'] {
|
||||
padding-top: $popper-margin-from-ref;
|
||||
}
|
||||
.popper[data-placement^='bottom-end'] .popper__arrow {
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-top-color: transparent;
|
||||
top: 0;
|
||||
left: calc(100% - 5px);
|
||||
}
|
||||
|
||||
// Right
|
||||
.popper[data-placement^='right'] {
|
||||
padding-left: $popper-margin-from-ref;
|
||||
}
|
||||
.popper[data-placement^='right'] .popper__arrow {
|
||||
border-width: 5px 5px 5px 0;
|
||||
border-left-color: transparent;
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
left: 0;
|
||||
top: calc(50% - 5px);
|
||||
}
|
||||
|
||||
// Left
|
||||
.popper[data-placement^='left'] {
|
||||
padding-right: $popper-margin-from-ref;
|
||||
}
|
||||
.popper[data-placement^='left'] .popper__arrow {
|
||||
border-width: 5px 0 5px 5px;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
right: -5px;
|
||||
top: calc(50% - 5px);
|
||||
}
|
||||
|
||||
.popper__target,
|
||||
.popper__manager {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.popper__manager--block {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Tooltip renders correctly 1`] = `
|
||||
<a
|
||||
className="test-class"
|
||||
href="http://www.grafana.com"
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
>
|
||||
Link with tooltip
|
||||
</a>
|
||||
`;
|
||||
@@ -1 +1,7 @@
|
||||
@import 'CustomScrollbar/CustomScrollbar';
|
||||
@import 'DeleteButton/DeleteButton';
|
||||
@import 'ThresholdsEditor/ThresholdsEditor';
|
||||
@import 'Tooltip/Tooltip';
|
||||
@import 'Select/Select';
|
||||
@import 'PanelOptionsGroup/PanelOptionsGroup';
|
||||
@import 'PanelOptionsGrid/PanelOptionsGrid';
|
||||
|
||||
@@ -1 +1,20 @@
|
||||
export { DeleteButton } from './DeleteButton/DeleteButton';
|
||||
export { Tooltip } from './Tooltip/Tooltip';
|
||||
export { Portal } from './Portal/Portal';
|
||||
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';
|
||||
|
||||
// Select
|
||||
export { Select, AsyncSelect, SelectOptionItem } from './Select/Select';
|
||||
export { IndicatorsContainer } from './Select/IndicatorsContainer';
|
||||
export { NoOptionsMessage } from './Select/NoOptionsMessage';
|
||||
export { default as resetSelectStyles } from './Select/resetSelectStyles';
|
||||
|
||||
export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
|
||||
export { ColorPicker } from './ColorPicker/ColorPicker';
|
||||
export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover';
|
||||
export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker';
|
||||
export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
|
||||
export { GfFormLabel } from './GfFormLabel/GfFormLabel';
|
||||
export { Graph } from './Graph/Graph';
|
||||
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
|
||||
export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { GfFormLabel } from './GfFormLabel/GfFormLabel';
|
||||
@@ -1,5 +1,3 @@
|
||||
export * from './components';
|
||||
export * from './visualizations';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
export * from './forms';
|
||||
|
||||
16
packages/grafana-ui/src/types/gauge.ts
Normal file
16
packages/grafana-ui/src/types/gauge.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { RangeMap, Threshold, ValueMap } from './panel';
|
||||
|
||||
export interface GaugeOptions {
|
||||
baseColor: string;
|
||||
decimals: number;
|
||||
mappings: Array<RangeMap | ValueMap>;
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
prefix: string;
|
||||
showThresholdLabels: boolean;
|
||||
showThresholdMarkers: boolean;
|
||||
stat: string;
|
||||
suffix: string;
|
||||
thresholds: Threshold[];
|
||||
unit: string;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './series';
|
||||
export * from './time';
|
||||
export * from './panel';
|
||||
export * from './gauge';
|
||||
|
||||
@@ -29,3 +29,35 @@ export interface PanelMenuItem {
|
||||
shortcut?: string;
|
||||
subMenu?: PanelMenuItem[];
|
||||
}
|
||||
|
||||
export interface Threshold {
|
||||
index: number;
|
||||
value: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export enum BasicGaugeColor {
|
||||
Green = '#299c46',
|
||||
Red = '#d44a3a',
|
||||
}
|
||||
|
||||
export enum MappingType {
|
||||
ValueToText = 1,
|
||||
RangeToText = 2,
|
||||
}
|
||||
|
||||
interface BaseMap {
|
||||
id: number;
|
||||
operator: string;
|
||||
text: string;
|
||||
type: MappingType;
|
||||
}
|
||||
|
||||
export interface ValueMap extends BaseMap {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface RangeMap extends BaseMap {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
93
packages/grafana-ui/src/utils/colors.ts
Normal file
93
packages/grafana-ui/src/utils/colors.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import _ from 'lodash';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
export const PALETTE_ROWS = 4;
|
||||
export const PALETTE_COLUMNS = 14;
|
||||
export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)';
|
||||
export const OK_COLOR = 'rgba(11, 237, 50, 1)';
|
||||
export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)';
|
||||
export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)';
|
||||
export const PENDING_COLOR = 'rgba(247, 149, 32, 1)';
|
||||
export const REGION_FILL_ALPHA = 0.09;
|
||||
|
||||
export const colors = [
|
||||
'#7EB26D', // 0: pale green
|
||||
'#EAB839', // 1: mustard
|
||||
'#6ED0E0', // 2: light blue
|
||||
'#EF843C', // 3: orange
|
||||
'#E24D42', // 4: red
|
||||
'#1F78C1', // 5: ocean
|
||||
'#BA43A9', // 6: purple
|
||||
'#705DA0', // 7: violet
|
||||
'#508642', // 8: dark green
|
||||
'#CCA300', // 9: dark sand
|
||||
'#447EBC',
|
||||
'#C15C17',
|
||||
'#890F02',
|
||||
'#0A437C',
|
||||
'#6D1F62',
|
||||
'#584477',
|
||||
'#B7DBAB',
|
||||
'#F4D598',
|
||||
'#70DBED',
|
||||
'#F9BA8F',
|
||||
'#F29191',
|
||||
'#82B5D8',
|
||||
'#E5A8E2',
|
||||
'#AEA2E0',
|
||||
'#629E51',
|
||||
'#E5AC0E',
|
||||
'#64B0C8',
|
||||
'#E0752D',
|
||||
'#BF1B00',
|
||||
'#0A50A1',
|
||||
'#962D82',
|
||||
'#614D93',
|
||||
'#9AC48A',
|
||||
'#F2C96D',
|
||||
'#65C5DB',
|
||||
'#F9934E',
|
||||
'#EA6460',
|
||||
'#5195CE',
|
||||
'#D683CE',
|
||||
'#806EB7',
|
||||
'#3F6833',
|
||||
'#967302',
|
||||
'#2F575E',
|
||||
'#99440A',
|
||||
'#58140C',
|
||||
'#052B51',
|
||||
'#511749',
|
||||
'#3F2B5B',
|
||||
'#E0F9D7',
|
||||
'#FCEACA',
|
||||
'#CFFAFF',
|
||||
'#F9E2D2',
|
||||
'#FCE2DE',
|
||||
'#BADFF4',
|
||||
'#F9D9F9',
|
||||
'#DEDAF7',
|
||||
];
|
||||
|
||||
function sortColorsByHue(hexColors: string[]) {
|
||||
const hslColors = _.map(hexColors, hexToHsl);
|
||||
|
||||
const sortedHSLColors = _.sortBy(hslColors, ['h']);
|
||||
const chunkedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS);
|
||||
const sortedChunkedHSLColors = _.map(chunkedHSLColors, chunk => {
|
||||
return _.sortBy(chunk, 'l');
|
||||
});
|
||||
const flattenedZippedSortedChunkedHSLColors = _.flattenDeep(_.zip(...sortedChunkedHSLColors));
|
||||
|
||||
return _.map(flattenedZippedSortedChunkedHSLColors, hslToHex);
|
||||
}
|
||||
|
||||
function hexToHsl(color: string) {
|
||||
return tinycolor(color).toHsl();
|
||||
}
|
||||
|
||||
function hslToHex(color: any) {
|
||||
return tinycolor(color).toHexString();
|
||||
}
|
||||
|
||||
export let sortedColors = sortColorsByHue(colors);
|
||||
@@ -1 +1,3 @@
|
||||
export * from './processTimeSeries';
|
||||
export * from './valueFormats/valueFormats';
|
||||
export * from './colors';
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { toHex, toHex0x } from './arithmeticFormatters';
|
||||
|
||||
describe('hex', () => {
|
||||
it('positive integer', () => {
|
||||
const str = toHex(100, 0);
|
||||
expect(str).toBe('64');
|
||||
});
|
||||
it('negative integer', () => {
|
||||
const str = toHex(-100, 0);
|
||||
expect(str).toBe('-64');
|
||||
});
|
||||
it('positive float', () => {
|
||||
const str = toHex(50.52, 1);
|
||||
expect(str).toBe('32.8');
|
||||
});
|
||||
it('negative float', () => {
|
||||
const str = toHex(-50.333, 2);
|
||||
expect(str).toBe('-32.547AE147AE14');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hex 0x', () => {
|
||||
it('positive integeter', () => {
|
||||
const str = toHex0x(7999, 0);
|
||||
expect(str).toBe('0x1F3F');
|
||||
});
|
||||
it('negative integer', () => {
|
||||
const str = toHex0x(-584, 0);
|
||||
expect(str).toBe('-0x248');
|
||||
});
|
||||
|
||||
it('positive float', () => {
|
||||
const str = toHex0x(74.443, 3);
|
||||
expect(str).toBe('0x4A.716872B020C4');
|
||||
});
|
||||
it('negative float', () => {
|
||||
const str = toHex0x(-65.458, 1);
|
||||
expect(str).toBe('-0x41.8');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { toFixed } from './valueFormats';
|
||||
|
||||
export function toPercent(size: number, decimals: number) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
return toFixed(size, decimals) + '%';
|
||||
}
|
||||
|
||||
export function toPercentUnit(size: number, decimals: number) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
return toFixed(100 * size, decimals) + '%';
|
||||
}
|
||||
|
||||
export function toHex0x(value: number, decimals: number) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
const hexString = toHex(value, decimals);
|
||||
if (hexString.substring(0, 1) === '-') {
|
||||
return '-0x' + hexString.substring(1);
|
||||
}
|
||||
return '0x' + hexString;
|
||||
}
|
||||
|
||||
export function toHex(value: number, decimals: number) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
return parseFloat(toFixed(value, decimals))
|
||||
.toString(16)
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
export function sci(value: number, decimals: number) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
return value.toExponential(decimals);
|
||||
}
|
||||
322
packages/grafana-ui/src/utils/valueFormats/categories.ts
Normal file
322
packages/grafana-ui/src/utils/valueFormats/categories.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
import { locale, scaledUnits, simpleCountUnit, toFixed, toFixedUnit, ValueFormatCategory } from './valueFormats';
|
||||
import {
|
||||
dateTimeAsIso,
|
||||
dateTimeAsUS,
|
||||
dateTimeFromNow,
|
||||
toClockMilliseconds,
|
||||
toClockSeconds,
|
||||
toDays,
|
||||
toDurationInHoursMinutesSeconds,
|
||||
toDurationInMilliseconds,
|
||||
toDurationInSeconds,
|
||||
toHours,
|
||||
toMicroSeconds,
|
||||
toMilliSeconds,
|
||||
toMinutes,
|
||||
toNanoSeconds,
|
||||
toSeconds,
|
||||
toTimeTicks,
|
||||
} from './dateTimeFormatters';
|
||||
import { toHex, sci, toHex0x, toPercent, toPercentUnit } from './arithmeticFormatters';
|
||||
import { binarySIPrefix, currency, decimalSIPrefix } from './symbolFormatters';
|
||||
|
||||
export const getCategories = (): ValueFormatCategory[] => [
|
||||
{
|
||||
name: 'Misc',
|
||||
formats: [
|
||||
{ name: 'none', id: 'none', fn: toFixed },
|
||||
{
|
||||
name: 'short',
|
||||
id: 'short',
|
||||
fn: scaledUnits(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Quadr', ' Quint', ' Sext', ' Sept']),
|
||||
},
|
||||
{ name: 'percent (0-100)', id: 'percent', fn: toPercent },
|
||||
{ name: 'percent (0.0-1.0)', id: 'percentunit', fn: toPercentUnit },
|
||||
{ name: 'Humidity (%H)', id: 'humidity', fn: toFixedUnit('%H') },
|
||||
{ name: 'decibel', id: 'dB', fn: toFixedUnit('dB') },
|
||||
{ name: 'hexadecimal (0x)', id: 'hex0x', fn: toHex0x },
|
||||
{ name: 'hexadecimal', id: 'hex', fn: toHex },
|
||||
{ name: 'scientific notation', id: 'sci', fn: sci },
|
||||
{ name: 'locale format', id: 'locale', fn: locale },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Acceleration',
|
||||
formats: [
|
||||
{ name: 'Meters/sec²', id: 'accMS2', fn: toFixedUnit('m/sec²') },
|
||||
{ name: 'Feet/sec²', id: 'accFS2', fn: toFixedUnit('f/sec²') },
|
||||
{ name: 'G unit', id: 'accG', fn: toFixedUnit('g') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Angle',
|
||||
formats: [
|
||||
{ name: 'Degrees (°)', id: 'degree', fn: toFixedUnit('°') },
|
||||
{ name: 'Radians', id: 'radian', fn: toFixedUnit('rad') },
|
||||
{ name: 'Gradian', id: 'grad', fn: toFixedUnit('grad') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Area',
|
||||
formats: [
|
||||
{ name: 'Square Meters (m²)', id: 'areaM2', fn: toFixedUnit('m²') },
|
||||
{ name: 'Square Feet (ft²)', id: 'areaF2', fn: toFixedUnit('ft²') },
|
||||
{ name: 'Square Miles (mi²)', id: 'areaMI2', fn: toFixedUnit('mi²') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Computation',
|
||||
formats: [
|
||||
{ name: 'FLOP/s', id: 'flops', fn: decimalSIPrefix('FLOP/s') },
|
||||
{ name: 'MFLOP/s', id: 'mflops', fn: decimalSIPrefix('FLOP/s', 2) },
|
||||
{ name: 'GFLOP/s', id: 'gflops', fn: decimalSIPrefix('FLOP/s', 3) },
|
||||
{ name: 'TFLOP/s', id: 'tflops', fn: decimalSIPrefix('FLOP/s', 4) },
|
||||
{ name: 'PFLOP/s', id: 'pflops', fn: decimalSIPrefix('FLOP/s', 5) },
|
||||
{ name: 'EFLOP/s', id: 'eflops', fn: decimalSIPrefix('FLOP/s', 6) },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Concentration',
|
||||
formats: [
|
||||
{ name: 'parts-per-million (ppm)', id: 'ppm', fn: toFixedUnit('ppm') },
|
||||
{ name: 'parts-per-billion (ppb)', id: 'conppb', fn: toFixedUnit('ppb') },
|
||||
{ name: 'nanogram per cubic meter (ng/m³)', id: 'conngm3', fn: toFixedUnit('ng/m³') },
|
||||
{ name: 'nanogram per normal cubic meter (ng/Nm³)', id: 'conngNm3', fn: toFixedUnit('ng/Nm³') },
|
||||
{ name: 'microgram per cubic meter (μg/m³)', id: 'conμgm3', fn: toFixedUnit('μg/m³') },
|
||||
{ name: 'microgram per normal cubic meter (μg/Nm³)', id: 'conμgNm3', fn: toFixedUnit('μg/Nm³') },
|
||||
{ name: 'milligram per cubic meter (mg/m³)', id: 'conmgm3', fn: toFixedUnit('mg/m³') },
|
||||
{ name: 'milligram per normal cubic meter (mg/Nm³)', id: 'conmgNm3', fn: toFixedUnit('mg/Nm³') },
|
||||
{ name: 'gram per cubic meter (g/m³)', id: 'congm3', fn: toFixedUnit('g/m³') },
|
||||
{ name: 'gram per normal cubic meter (g/Nm³)', id: 'congNm3', fn: toFixedUnit('g/Nm³') },
|
||||
{ name: 'milligrams per decilitre (mg/dL)', id: 'conmgdL', fn: toFixedUnit('mg/dL') },
|
||||
{ name: 'millimoles per litre (mmol/L)', id: 'conmmolL', fn: toFixedUnit('mmol/L') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Currency',
|
||||
formats: [
|
||||
{ name: 'Dollars ($)', id: 'currencyUSD', fn: currency('$') },
|
||||
{ name: 'Pounds (£)', id: 'currencyGBP', fn: currency('£') },
|
||||
{ name: 'Euro (€)', id: 'currencyEUR', fn: currency('€') },
|
||||
{ name: 'Yen (¥)', id: 'currencyJPY', fn: currency('¥') },
|
||||
{ name: 'Rubles (₽)', id: 'currencyRUB', fn: currency('₽') },
|
||||
{ name: 'Hryvnias (₴)', id: 'currencyUAH', fn: currency('₴') },
|
||||
{ name: 'Real (R$)', id: 'currencyBRL', fn: currency('R$') },
|
||||
{ name: 'Danish Krone (kr)', id: 'currencyDKK', fn: currency('kr') },
|
||||
{ name: 'Icelandic Króna (kr)', id: 'currencyISK', fn: currency('kr') },
|
||||
{ name: 'Norwegian Krone (kr)', id: 'currencyNOK', fn: currency('kr') },
|
||||
{ name: 'Swedish Krona (kr)', id: 'currencySEK', fn: currency('kr') },
|
||||
{ name: 'Czech koruna (czk)', id: 'currencyCZK', fn: currency('czk') },
|
||||
{ name: 'Swiss franc (CHF)', id: 'currencyCHF', fn: currency('CHF') },
|
||||
{ name: 'Polish Złoty (PLN)', id: 'currencyPLN', fn: currency('PLN') },
|
||||
{ name: 'Bitcoin (฿)', id: 'currencyBTC', fn: currency('฿') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Data (IEC)',
|
||||
formats: [
|
||||
{ name: 'bits', id: 'bits', fn: binarySIPrefix('b') },
|
||||
{ name: 'bytes', id: 'bytes', fn: binarySIPrefix('B') },
|
||||
{ name: 'kibibytes', id: 'kbytes', fn: binarySIPrefix('B', 1) },
|
||||
{ name: 'mebibytes', id: 'mbytes', fn: binarySIPrefix('B', 2) },
|
||||
{ name: 'gibibytes', id: 'gbytes', fn: binarySIPrefix('B', 3) },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Data (Metric)',
|
||||
formats: [
|
||||
{ name: 'bits', id: 'decbits', fn: decimalSIPrefix('d') },
|
||||
{ name: 'bytes', id: 'decbytes', fn: decimalSIPrefix('B') },
|
||||
{ name: 'kilobytes', id: 'deckbytes', fn: decimalSIPrefix('B', 1) },
|
||||
{ name: 'megabytes', id: 'decmbytes', fn: decimalSIPrefix('B', 2) },
|
||||
{ name: 'gigabytes', id: 'decgbytes', fn: decimalSIPrefix('B', 3) },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Data Rate',
|
||||
formats: [
|
||||
{ name: 'packets/sec', id: 'pps', fn: decimalSIPrefix('pps') },
|
||||
{ name: 'bits/sec', id: 'bps', fn: decimalSIPrefix('bps') },
|
||||
{ name: 'bytes/sec', id: 'Bps', fn: decimalSIPrefix('B/s') },
|
||||
{ name: 'kilobytes/sec', id: 'KBs', fn: decimalSIPrefix('Bs', 1) },
|
||||
{ name: 'kilobits/sec', id: 'Kbits', fn: decimalSIPrefix('bps', 1) },
|
||||
{ name: 'megabytes/sec', id: 'MBs', fn: decimalSIPrefix('Bs', 2) },
|
||||
{ name: 'megabits/sec', id: 'Mbits', fn: decimalSIPrefix('bps', 2) },
|
||||
{ name: 'gigabytes/sec', id: 'GBs', fn: decimalSIPrefix('Bs', 3) },
|
||||
{ name: 'gigabits/sec', id: 'Gbits', fn: decimalSIPrefix('bps', 3) },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Date & Time',
|
||||
formats: [
|
||||
{ name: 'YYYY-MM-DD HH:mm:ss', id: 'dateTimeAsIso', fn: dateTimeAsIso },
|
||||
{ name: 'DD/MM/YYYY h:mm:ss a', id: 'dateTimeAsUS', fn: dateTimeAsUS },
|
||||
{ name: 'From Now', id: 'dateTimeFromNow', fn: dateTimeFromNow },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Energy',
|
||||
formats: [
|
||||
{ name: 'Watt (W)', id: 'watt', fn: decimalSIPrefix('W') },
|
||||
{ name: 'Kilowatt (kW)', id: 'kwatt', fn: decimalSIPrefix('W', 1) },
|
||||
{ name: 'Milliwatt (mW)', id: 'mwatt', fn: decimalSIPrefix('W', -1) },
|
||||
{ name: 'Watt per square meter (W/m²)', id: 'Wm2', fn: toFixedUnit('W/m²') },
|
||||
{ name: 'Volt-ampere (VA)', id: 'voltamp', fn: decimalSIPrefix('VA') },
|
||||
{ name: 'Kilovolt-ampere (kVA)', id: 'kvoltamp', fn: decimalSIPrefix('VA', 1) },
|
||||
{ name: 'Volt-ampere reactive (var)', id: 'voltampreact', fn: decimalSIPrefix('var') },
|
||||
{ name: 'Kilovolt-ampere reactive (kvar)', id: 'kvoltampreact', fn: decimalSIPrefix('var', 1) },
|
||||
{ name: 'Watt-hour (Wh)', id: 'watth', fn: decimalSIPrefix('Wh') },
|
||||
{ name: 'Kilowatt-hour (kWh)', id: 'kwatth', fn: decimalSIPrefix('Wh', 1) },
|
||||
{ name: 'Kilowatt-min (kWm)', id: 'kwattm', fn: decimalSIPrefix('W/Min', 1) },
|
||||
{ name: 'Joule (J)', id: 'joule', fn: decimalSIPrefix('J') },
|
||||
{ name: 'Electron volt (eV)', id: 'ev', fn: decimalSIPrefix('eV') },
|
||||
{ name: 'Ampere (A)', id: 'amp', fn: decimalSIPrefix('A') },
|
||||
{ name: 'Kiloampere (kA)', id: 'kamp', fn: decimalSIPrefix('A', 1) },
|
||||
{ name: 'Milliampere (mA)', id: 'mamp', fn: decimalSIPrefix('A', -1) },
|
||||
{ name: 'Volt (V)', id: 'volt', fn: decimalSIPrefix('V') },
|
||||
{ name: 'Kilovolt (kV)', id: 'kvolt', fn: decimalSIPrefix('V', 1) },
|
||||
{ name: 'Millivolt (mV)', id: 'mvolt', fn: decimalSIPrefix('V', -1) },
|
||||
{ name: 'Decibel-milliwatt (dBm)', id: 'dBm', fn: decimalSIPrefix('dBm') },
|
||||
{ name: 'Ohm (Ω)', id: 'ohm', fn: decimalSIPrefix('Ω') },
|
||||
{ name: 'Lumens (Lm)', id: 'lumens', fn: decimalSIPrefix('Lm') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Flow',
|
||||
formats: [
|
||||
{ name: 'Gallons/min (gpm)', id: 'flowgpm', fn: toFixedUnit('gpm') },
|
||||
{ name: 'Cubic meters/sec (cms)', id: 'flowcms', fn: toFixedUnit('cms') },
|
||||
{ name: 'Cubic feet/sec (cfs)', id: 'flowcfs', fn: toFixedUnit('cfs') },
|
||||
{ name: 'Cubic feet/min (cfm)', id: 'flowcfm', fn: toFixedUnit('cfm') },
|
||||
{ name: 'Litre/hour', id: 'litreh', fn: toFixedUnit('l/h') },
|
||||
{ name: 'Litre/min (l/min)', id: 'flowlpm', fn: toFixedUnit('l/min') },
|
||||
{ name: 'milliLitre/min (mL/min)', id: 'flowmlpm', fn: toFixedUnit('mL/min') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Force',
|
||||
formats: [
|
||||
{ name: 'Newton-meters (Nm)', id: 'forceNm', fn: decimalSIPrefix('Nm') },
|
||||
{ name: 'Kilonewton-meters (kNm)', id: 'forcekNm', fn: decimalSIPrefix('Nm', 1) },
|
||||
{ name: 'Newtons (N)', id: 'forceN', fn: decimalSIPrefix('N') },
|
||||
{ name: 'Kilonewtons (kN)', id: 'forcekN', fn: decimalSIPrefix('N', 1) },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Hash Rate',
|
||||
formats: [
|
||||
{ name: 'hashes/sec', id: 'Hs', fn: decimalSIPrefix('H/s') },
|
||||
{ name: 'kilohashes/sec', id: 'KHs', fn: decimalSIPrefix('H/s', 1) },
|
||||
{ name: 'megahashes/sec', id: 'MHs', fn: decimalSIPrefix('H/s', 2) },
|
||||
{ name: 'gigahashes/sec', id: 'GHs', fn: decimalSIPrefix('H/s', 3) },
|
||||
{ name: 'terahashes/sec', id: 'THs', fn: decimalSIPrefix('H/s', 4) },
|
||||
{ name: 'petahashes/sec', id: 'PHs', fn: decimalSIPrefix('H/s', 5) },
|
||||
{ name: 'exahashes/sec', id: 'EHs', fn: decimalSIPrefix('H/s', 6) },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Mass',
|
||||
formats: [
|
||||
{ name: 'milligram (mg)', id: 'massmg', fn: decimalSIPrefix('g', -1) },
|
||||
{ name: 'gram (g)', id: 'massg', fn: decimalSIPrefix('g') },
|
||||
{ name: 'kilogram (kg)', id: 'masskg', fn: decimalSIPrefix('g', 1) },
|
||||
{ name: 'metric ton (t)', id: 'masst', fn: toFixedUnit('t') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'length',
|
||||
formats: [
|
||||
{ name: 'millimetre (mm)', id: 'lengthmm', fn: decimalSIPrefix('m', -1) },
|
||||
{ name: 'feet (ft)', id: 'lengthft', fn: toFixedUnit('ft') },
|
||||
{ name: 'meter (m)', id: 'lengthm', fn: decimalSIPrefix('m') },
|
||||
{ name: 'kilometer (km)', id: 'lengthkm', fn: decimalSIPrefix('m', 1) },
|
||||
{ name: 'mile (mi)', id: 'lengthmi', fn: toFixedUnit('mi') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Pressure',
|
||||
formats: [
|
||||
{ name: 'Millibars', id: 'pressurembar', fn: decimalSIPrefix('bar', -1) },
|
||||
{ name: 'Bars', id: 'pressurebar', fn: decimalSIPrefix('bar') },
|
||||
{ name: 'Kilobars', id: 'pressurekbar', fn: decimalSIPrefix('bar', 1) },
|
||||
{ name: 'Hectopascals', id: 'pressurehpa', fn: toFixedUnit('hPa') },
|
||||
{ name: 'Kilopascals', id: 'pressurekpa', fn: toFixedUnit('kPa') },
|
||||
{ name: 'Inches of mercury', id: 'pressurehg', fn: toFixedUnit('"Hg') },
|
||||
{ name: 'PSI', id: 'pressurepsi', fn: scaledUnits(1000, ['psi', 'ksi', 'Mpsi']) },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Radiation',
|
||||
formats: [
|
||||
{ name: 'Becquerel (Bq)', id: 'radbq', fn: decimalSIPrefix('Bq') },
|
||||
{ name: 'curie (Ci)', id: 'radci', fn: decimalSIPrefix('Ci') },
|
||||
{ name: 'Gray (Gy)', id: 'radgy', fn: decimalSIPrefix('Gy') },
|
||||
{ name: 'rad', id: 'radrad', fn: decimalSIPrefix('rad') },
|
||||
{ name: 'Sievert (Sv)', id: 'radsv', fn: decimalSIPrefix('Sv') },
|
||||
{ name: 'rem', id: 'radrem', fn: decimalSIPrefix('rem') },
|
||||
{ name: 'Exposure (C/kg)', id: 'radexpckg', fn: decimalSIPrefix('C/kg') },
|
||||
{ name: 'roentgen (R)', id: 'radr', fn: decimalSIPrefix('R') },
|
||||
{ name: 'Sievert/hour (Sv/h)', id: 'radsvh', fn: decimalSIPrefix('Sv/h') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Temperature',
|
||||
formats: [
|
||||
{ name: 'Celsius (°C)', id: 'celsius', fn: toFixedUnit('°C') },
|
||||
{ name: 'Farenheit (°F)', id: 'farenheit', fn: toFixedUnit('°F') },
|
||||
{ name: 'Kelvin (K)', id: 'kelvin', fn: toFixedUnit('K') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Time',
|
||||
formats: [
|
||||
{ name: 'Hertz (1/s)', id: 'hertz', fn: decimalSIPrefix('Hz') },
|
||||
{ name: 'nanoseconds (ns)', id: 'ns', fn: toNanoSeconds },
|
||||
{ name: 'microseconds (µs)', id: 'µs', fn: toMicroSeconds },
|
||||
{ name: 'milliseconds (ms)', id: 'ms', fn: toMilliSeconds },
|
||||
{ name: 'seconds (s)', id: 's', fn: toSeconds },
|
||||
{ name: 'minutes (m)', id: 'm', fn: toMinutes },
|
||||
{ name: 'hours (h)', id: 'h', fn: toHours },
|
||||
{ name: 'days (d)', id: 'd', fn: toDays },
|
||||
{ name: 'duration (ms)', id: 'dtdurationms', fn: toDurationInMilliseconds },
|
||||
{ name: 'duration (s)', id: 'dtdurations', fn: toDurationInSeconds },
|
||||
{ name: 'duration (hh:mm:ss)', id: 'dthms', fn: toDurationInHoursMinutesSeconds },
|
||||
{ name: 'Timeticks (s/100)', id: 'timeticks', fn: toTimeTicks },
|
||||
{ name: 'clock (ms)', id: 'clockms', fn: toClockMilliseconds },
|
||||
{ name: 'clock (s)', id: 'clocks', fn: toClockSeconds },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Throughput',
|
||||
formats: [
|
||||
{ name: 'ops/sec (ops)', id: 'ops', fn: simpleCountUnit('ops') },
|
||||
{ name: 'requests/sec (rps)', id: 'reqps', fn: simpleCountUnit('reqps') },
|
||||
{ name: 'reads/sec (rps)', id: 'rps', fn: simpleCountUnit('rps') },
|
||||
{ name: 'writes/sec (wps)', id: 'wps', fn: simpleCountUnit('wps') },
|
||||
{ name: 'I/O ops/sec (iops)', id: 'iops', fn: simpleCountUnit('iops') },
|
||||
{ name: 'ops/min (opm)', id: 'opm', fn: simpleCountUnit('opm') },
|
||||
{ name: 'reads/min (rpm)', id: 'rpm', fn: simpleCountUnit('rpm') },
|
||||
{ name: 'writes/min (wpm)', id: 'wpm', fn: simpleCountUnit('wpm') },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Velocity',
|
||||
formats: [
|
||||
{ name: 'metres/second (m/s)', id: 'velocityms', fn: toFixedUnit('m/s') },
|
||||
{ name: 'kilometers/hour (km/h)', id: 'velocitykmh', fn: toFixedUnit('km/h') },
|
||||
{ name: 'miles/hour (mph)', id: 'velocitymph', fn: toFixedUnit('mph') },
|
||||
{ name: 'knot (kn)', id: 'velocityknot', fn: toFixedUnit('kn') },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Volume',
|
||||
formats: [
|
||||
{ name: 'millilitre (mL)', id: 'mlitre', fn: decimalSIPrefix('L', -1) },
|
||||
{ name: 'litre (L)', id: 'litre', fn: decimalSIPrefix('L') },
|
||||
{ name: 'cubic metre', id: 'm3', fn: toFixedUnit('m³') },
|
||||
{ name: 'Normal cubic metre', id: 'Nm3', fn: toFixedUnit('Nm³') },
|
||||
{ name: 'cubic decimetre', id: 'dm3', fn: toFixedUnit('dm³') },
|
||||
{ name: 'gallons', id: 'gallons', fn: toFixedUnit('gal') },
|
||||
],
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,231 @@
|
||||
import moment from 'moment';
|
||||
import {
|
||||
dateTimeAsIso,
|
||||
dateTimeAsUS,
|
||||
dateTimeFromNow,
|
||||
Interval,
|
||||
toClock,
|
||||
toDuration,
|
||||
toDurationInMilliseconds,
|
||||
toDurationInSeconds,
|
||||
} from './dateTimeFormatters';
|
||||
|
||||
describe('date time formats', () => {
|
||||
const epoch = 1505634997920;
|
||||
const utcTime = moment.utc(epoch);
|
||||
const browserTime = moment(epoch);
|
||||
|
||||
it('should format as iso date', () => {
|
||||
const expected = browserTime.format('YYYY-MM-DD HH:mm:ss');
|
||||
const actual = dateTimeAsIso(epoch, 0, 0, false);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should format as iso date (in UTC)', () => {
|
||||
const expected = utcTime.format('YYYY-MM-DD HH:mm:ss');
|
||||
const actual = dateTimeAsIso(epoch, 0, 0, true);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should format as iso date and skip date when today', () => {
|
||||
const now = moment();
|
||||
const expected = now.format('HH:mm:ss');
|
||||
const actual = dateTimeAsIso(now.valueOf(), 0, 0, false);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should format as iso date (in UTC) and skip date when today', () => {
|
||||
const now = moment.utc();
|
||||
const expected = now.format('HH:mm:ss');
|
||||
const actual = dateTimeAsIso(now.valueOf(), 0, 0, true);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should format as US date', () => {
|
||||
const expected = browserTime.format('MM/DD/YYYY h:mm:ss a');
|
||||
const actual = dateTimeAsUS(epoch, 0, 0, false);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should format as US date (in UTC)', () => {
|
||||
const expected = utcTime.format('MM/DD/YYYY h:mm:ss a');
|
||||
const actual = dateTimeAsUS(epoch, 0, 0, true);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should format as US date and skip date when today', () => {
|
||||
const now = moment();
|
||||
const expected = now.format('h:mm:ss a');
|
||||
const actual = dateTimeAsUS(now.valueOf(), 0, 0, false);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should format as US date (in UTC) and skip date when today', () => {
|
||||
const now = moment.utc();
|
||||
const expected = now.format('h:mm:ss a');
|
||||
const actual = dateTimeAsUS(now.valueOf(), 0, 0, true);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should format as from now with days', () => {
|
||||
const daysAgo = moment().add(-7, 'd');
|
||||
const expected = '7 days ago';
|
||||
const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, false);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should format as from now with days (in UTC)', () => {
|
||||
const daysAgo = moment.utc().add(-7, 'd');
|
||||
const expected = '7 days ago';
|
||||
const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, true);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should format as from now with minutes', () => {
|
||||
const daysAgo = moment().add(-2, 'm');
|
||||
const expected = '2 minutes ago';
|
||||
const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, false);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should format as from now with minutes (in UTC)', () => {
|
||||
const daysAgo = moment.utc().add(-2, 'm');
|
||||
const expected = '2 minutes ago';
|
||||
const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, true);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('duration', () => {
|
||||
it('0 milliseconds', () => {
|
||||
const str = toDurationInMilliseconds(0, 0);
|
||||
expect(str).toBe('0 milliseconds');
|
||||
});
|
||||
it('1 millisecond', () => {
|
||||
const str = toDurationInMilliseconds(1, 0);
|
||||
expect(str).toBe('1 millisecond');
|
||||
});
|
||||
it('-1 millisecond', () => {
|
||||
const str = toDurationInMilliseconds(-1, 0);
|
||||
expect(str).toBe('1 millisecond ago');
|
||||
});
|
||||
it('seconds', () => {
|
||||
const str = toDurationInSeconds(1, 0);
|
||||
expect(str).toBe('1 second');
|
||||
});
|
||||
it('minutes', () => {
|
||||
const str = toDuration(1, 0, Interval.Minute);
|
||||
expect(str).toBe('1 minute');
|
||||
});
|
||||
it('hours', () => {
|
||||
const str = toDuration(1, 0, Interval.Hour);
|
||||
expect(str).toBe('1 hour');
|
||||
});
|
||||
it('days', () => {
|
||||
const str = toDuration(1, 0, Interval.Day);
|
||||
expect(str).toBe('1 day');
|
||||
});
|
||||
it('weeks', () => {
|
||||
const str = toDuration(1, 0, Interval.Week);
|
||||
expect(str).toBe('1 week');
|
||||
});
|
||||
it('months', () => {
|
||||
const str = toDuration(1, 0, Interval.Month);
|
||||
expect(str).toBe('1 month');
|
||||
});
|
||||
it('years', () => {
|
||||
const str = toDuration(1, 0, Interval.Year);
|
||||
expect(str).toBe('1 year');
|
||||
});
|
||||
it('decimal days', () => {
|
||||
const str = toDuration(1.5, 2, Interval.Day);
|
||||
expect(str).toBe('1 day, 12 hours, 0 minutes');
|
||||
});
|
||||
it('decimal months', () => {
|
||||
const str = toDuration(1.5, 3, Interval.Month);
|
||||
expect(str).toBe('1 month, 2 weeks, 1 day, 0 hours');
|
||||
});
|
||||
it('no decimals', () => {
|
||||
const str = toDuration(38898367008, 0, Interval.Millisecond);
|
||||
expect(str).toBe('1 year');
|
||||
});
|
||||
it('1 decimal', () => {
|
||||
const str = toDuration(38898367008, 1, Interval.Millisecond);
|
||||
expect(str).toBe('1 year, 2 months');
|
||||
});
|
||||
it('too many decimals', () => {
|
||||
const str = toDuration(38898367008, 20, Interval.Millisecond);
|
||||
expect(str).toBe('1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds');
|
||||
});
|
||||
it('floating point error', () => {
|
||||
const str = toDuration(36993906007, 8, Interval.Millisecond);
|
||||
expect(str).toBe('1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds');
|
||||
});
|
||||
});
|
||||
|
||||
describe('clock', () => {
|
||||
it('size less than 1 second', () => {
|
||||
const str = toClock(999, 0);
|
||||
expect(str).toBe('999ms');
|
||||
});
|
||||
describe('size less than 1 minute', () => {
|
||||
it('default', () => {
|
||||
const str = toClock(59999);
|
||||
expect(str).toBe('59s:999ms');
|
||||
});
|
||||
it('decimals equals 0', () => {
|
||||
const str = toClock(59999, 0);
|
||||
expect(str).toBe('59s');
|
||||
});
|
||||
});
|
||||
describe('size less than 1 hour', () => {
|
||||
it('default', () => {
|
||||
const str = toClock(3599999);
|
||||
expect(str).toBe('59m:59s:999ms');
|
||||
});
|
||||
it('decimals equals 0', () => {
|
||||
const str = toClock(3599999, 0);
|
||||
expect(str).toBe('59m');
|
||||
});
|
||||
it('decimals equals 1', () => {
|
||||
const str = toClock(3599999, 1);
|
||||
expect(str).toBe('59m:59s');
|
||||
});
|
||||
});
|
||||
describe('size greater than or equal 1 hour', () => {
|
||||
it('default', () => {
|
||||
const str = toClock(7199999);
|
||||
expect(str).toBe('01h:59m:59s:999ms');
|
||||
});
|
||||
it('decimals equals 0', () => {
|
||||
const str = toClock(7199999, 0);
|
||||
expect(str).toBe('01h');
|
||||
});
|
||||
it('decimals equals 1', () => {
|
||||
const str = toClock(7199999, 1);
|
||||
expect(str).toBe('01h:59m');
|
||||
});
|
||||
it('decimals equals 2', () => {
|
||||
const str = toClock(7199999, 2);
|
||||
expect(str).toBe('01h:59m:59s');
|
||||
});
|
||||
});
|
||||
describe('size greater than or equal 1 day', () => {
|
||||
it('default', () => {
|
||||
const str = toClock(89999999);
|
||||
expect(str).toBe('24h:59m:59s:999ms');
|
||||
});
|
||||
it('decimals equals 0', () => {
|
||||
const str = toClock(89999999, 0);
|
||||
expect(str).toBe('24h');
|
||||
});
|
||||
it('decimals equals 1', () => {
|
||||
const str = toClock(89999999, 1);
|
||||
expect(str).toBe('24h:59m');
|
||||
});
|
||||
it('decimals equals 2', () => {
|
||||
const str = toClock(89999999, 2);
|
||||
expect(str).toBe('24h:59m:59s');
|
||||
});
|
||||
});
|
||||
});
|
||||
312
packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts
Normal file
312
packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
import { toFixed, toFixedScaled } from './valueFormats';
|
||||
import moment from 'moment';
|
||||
|
||||
interface IntervalsInSeconds {
|
||||
[interval: string]: number;
|
||||
}
|
||||
|
||||
export enum Interval {
|
||||
Year = 'year',
|
||||
Month = 'month',
|
||||
Week = 'week',
|
||||
Day = 'day',
|
||||
Hour = 'hour',
|
||||
Minute = 'minute',
|
||||
Second = 'second',
|
||||
Millisecond = 'millisecond',
|
||||
}
|
||||
|
||||
const INTERVALS_IN_SECONDS: IntervalsInSeconds = {
|
||||
[Interval.Year]: 31536000,
|
||||
[Interval.Month]: 2592000,
|
||||
[Interval.Week]: 604800,
|
||||
[Interval.Day]: 86400,
|
||||
[Interval.Hour]: 3600,
|
||||
[Interval.Minute]: 60,
|
||||
[Interval.Second]: 1,
|
||||
[Interval.Millisecond]: 0.001,
|
||||
};
|
||||
|
||||
export function toNanoSeconds(size: number, decimals: number, scaledDecimals: number) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (Math.abs(size) < 1000) {
|
||||
return toFixed(size, decimals) + ' ns';
|
||||
} else if (Math.abs(size) < 1000000) {
|
||||
return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' µs');
|
||||
} else if (Math.abs(size) < 1000000000) {
|
||||
return toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' ms');
|
||||
} else if (Math.abs(size) < 60000000000) {
|
||||
return toFixedScaled(size / 1000000000, decimals, scaledDecimals, 9, ' s');
|
||||
} else {
|
||||
return toFixedScaled(size / 60000000000, decimals, scaledDecimals, 12, ' min');
|
||||
}
|
||||
}
|
||||
|
||||
export function toMicroSeconds(size: number, decimals: number, scaledDecimals: number) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (Math.abs(size) < 1000) {
|
||||
return toFixed(size, decimals) + ' µs';
|
||||
} else if (Math.abs(size) < 1000000) {
|
||||
return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' ms');
|
||||
} else {
|
||||
return toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' s');
|
||||
}
|
||||
}
|
||||
|
||||
export function toMilliSeconds(size: number, decimals: number, scaledDecimals: number) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (Math.abs(size) < 1000) {
|
||||
return toFixed(size, decimals) + ' ms';
|
||||
} else if (Math.abs(size) < 60000) {
|
||||
// Less than 1 min
|
||||
return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' s');
|
||||
} else if (Math.abs(size) < 3600000) {
|
||||
// Less than 1 hour, divide in minutes
|
||||
return toFixedScaled(size / 60000, decimals, scaledDecimals, 5, ' min');
|
||||
} else if (Math.abs(size) < 86400000) {
|
||||
// Less than one day, divide in hours
|
||||
return toFixedScaled(size / 3600000, decimals, scaledDecimals, 7, ' hour');
|
||||
} else if (Math.abs(size) < 31536000000) {
|
||||
// Less than one year, divide in days
|
||||
return toFixedScaled(size / 86400000, decimals, scaledDecimals, 8, ' day');
|
||||
}
|
||||
|
||||
return toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, ' year');
|
||||
}
|
||||
|
||||
export function toSeconds(size: number, decimals: number, scaledDecimals: number) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Less than 1 µs, divide in ns
|
||||
if (Math.abs(size) < 0.000001) {
|
||||
return toFixedScaled(size * 1e9, decimals, scaledDecimals - decimals, -9, ' ns');
|
||||
}
|
||||
// Less than 1 ms, divide in µs
|
||||
if (Math.abs(size) < 0.001) {
|
||||
return toFixedScaled(size * 1e6, decimals, scaledDecimals - decimals, -6, ' µs');
|
||||
}
|
||||
// Less than 1 second, divide in ms
|
||||
if (Math.abs(size) < 1) {
|
||||
return toFixedScaled(size * 1e3, decimals, scaledDecimals - decimals, -3, ' ms');
|
||||
}
|
||||
|
||||
if (Math.abs(size) < 60) {
|
||||
return toFixed(size, decimals) + ' s';
|
||||
} else if (Math.abs(size) < 3600) {
|
||||
// Less than 1 hour, divide in minutes
|
||||
return toFixedScaled(size / 60, decimals, scaledDecimals, 1, ' min');
|
||||
} else if (Math.abs(size) < 86400) {
|
||||
// Less than one day, divide in hours
|
||||
return toFixedScaled(size / 3600, decimals, scaledDecimals, 4, ' hour');
|
||||
} else if (Math.abs(size) < 604800) {
|
||||
// Less than one week, divide in days
|
||||
return toFixedScaled(size / 86400, decimals, scaledDecimals, 5, ' day');
|
||||
} else if (Math.abs(size) < 31536000) {
|
||||
// Less than one year, divide in week
|
||||
return toFixedScaled(size / 604800, decimals, scaledDecimals, 6, ' week');
|
||||
}
|
||||
|
||||
return toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year');
|
||||
}
|
||||
|
||||
export function toMinutes(size: number, decimals: number, scaledDecimals: number) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (Math.abs(size) < 60) {
|
||||
return toFixed(size, decimals) + ' min';
|
||||
} else if (Math.abs(size) < 1440) {
|
||||
return toFixedScaled(size / 60, decimals, scaledDecimals, 2, ' hour');
|
||||
} else if (Math.abs(size) < 10080) {
|
||||
return toFixedScaled(size / 1440, decimals, scaledDecimals, 3, ' day');
|
||||
} else if (Math.abs(size) < 604800) {
|
||||
return toFixedScaled(size / 10080, decimals, scaledDecimals, 4, ' week');
|
||||
} else {
|
||||
return toFixedScaled(size / 5.25948e5, decimals, scaledDecimals, 5, ' year');
|
||||
}
|
||||
}
|
||||
|
||||
export function toHours(size: number, decimals: number, scaledDecimals: number) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (Math.abs(size) < 24) {
|
||||
return toFixed(size, decimals) + ' hour';
|
||||
} else if (Math.abs(size) < 168) {
|
||||
return toFixedScaled(size / 24, decimals, scaledDecimals, 2, ' day');
|
||||
} else if (Math.abs(size) < 8760) {
|
||||
return toFixedScaled(size / 168, decimals, scaledDecimals, 3, ' week');
|
||||
} else {
|
||||
return toFixedScaled(size / 8760, decimals, scaledDecimals, 4, ' year');
|
||||
}
|
||||
}
|
||||
|
||||
export function toDays(size: number, decimals: number, scaledDecimals: number) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (Math.abs(size) < 7) {
|
||||
return toFixed(size, decimals) + ' day';
|
||||
} else if (Math.abs(size) < 365) {
|
||||
return toFixedScaled(size / 7, decimals, scaledDecimals, 2, ' week');
|
||||
} else {
|
||||
return toFixedScaled(size / 365, decimals, scaledDecimals, 3, ' year');
|
||||
}
|
||||
}
|
||||
|
||||
export function toDuration(size: number, decimals: number, timeScale: Interval): string {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
if (size === 0) {
|
||||
return '0 ' + timeScale + 's';
|
||||
}
|
||||
if (size < 0) {
|
||||
return toDuration(-size, decimals, timeScale) + ' ago';
|
||||
}
|
||||
|
||||
const units = [
|
||||
{ long: Interval.Year },
|
||||
{ long: Interval.Month },
|
||||
{ long: Interval.Week },
|
||||
{ long: Interval.Day },
|
||||
{ long: Interval.Hour },
|
||||
{ long: Interval.Minute },
|
||||
{ long: Interval.Second },
|
||||
{ long: Interval.Millisecond },
|
||||
];
|
||||
// convert $size to milliseconds
|
||||
// intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors
|
||||
size *= INTERVALS_IN_SECONDS[timeScale] * 1000;
|
||||
|
||||
const strings = [];
|
||||
// after first value >= 1 print only $decimals more
|
||||
let decrementDecimals = false;
|
||||
for (let i = 0; i < units.length && decimals >= 0; i++) {
|
||||
const interval = INTERVALS_IN_SECONDS[units[i].long] * 1000;
|
||||
const value = size / interval;
|
||||
if (value >= 1 || decrementDecimals) {
|
||||
decrementDecimals = true;
|
||||
const floor = Math.floor(value);
|
||||
const unit = units[i].long + (floor !== 1 ? 's' : '');
|
||||
strings.push(floor + ' ' + unit);
|
||||
size = size % interval;
|
||||
decimals--;
|
||||
}
|
||||
}
|
||||
|
||||
return strings.join(', ');
|
||||
}
|
||||
|
||||
export function toClock(size: number, decimals?: number) {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// < 1 second
|
||||
if (size < 1000) {
|
||||
return moment.utc(size).format('SSS\\m\\s');
|
||||
}
|
||||
|
||||
// < 1 minute
|
||||
if (size < 60000) {
|
||||
let format = 'ss\\s:SSS\\m\\s';
|
||||
if (decimals === 0) {
|
||||
format = 'ss\\s';
|
||||
}
|
||||
return moment.utc(size).format(format);
|
||||
}
|
||||
|
||||
// < 1 hour
|
||||
if (size < 3600000) {
|
||||
let format = 'mm\\m:ss\\s:SSS\\m\\s';
|
||||
if (decimals === 0) {
|
||||
format = 'mm\\m';
|
||||
} else if (decimals === 1) {
|
||||
format = 'mm\\m:ss\\s';
|
||||
}
|
||||
return moment.utc(size).format(format);
|
||||
}
|
||||
|
||||
let format = 'mm\\m:ss\\s:SSS\\m\\s';
|
||||
|
||||
const hours = `${('0' + Math.floor(moment.duration(size, 'milliseconds').asHours())).slice(-2)}h`;
|
||||
|
||||
if (decimals === 0) {
|
||||
format = '';
|
||||
} else if (decimals === 1) {
|
||||
format = 'mm\\m';
|
||||
} else if (decimals === 2) {
|
||||
format = 'mm\\m:ss\\s';
|
||||
}
|
||||
|
||||
return format ? `${hours}:${moment.utc(size).format(format)}` : hours;
|
||||
}
|
||||
|
||||
export function toDurationInMilliseconds(size: number, decimals: number) {
|
||||
return toDuration(size, decimals, Interval.Millisecond);
|
||||
}
|
||||
|
||||
export function toDurationInSeconds(size: number, decimals: number) {
|
||||
return toDuration(size, decimals, Interval.Second);
|
||||
}
|
||||
|
||||
export function toDurationInHoursMinutesSeconds(size: number) {
|
||||
const strings = [];
|
||||
const numHours = Math.floor(size / 3600);
|
||||
const numMinutes = Math.floor((size % 3600) / 60);
|
||||
const numSeconds = Math.floor((size % 3600) % 60);
|
||||
numHours > 9 ? strings.push('' + numHours) : strings.push('0' + numHours);
|
||||
numMinutes > 9 ? strings.push('' + numMinutes) : strings.push('0' + numMinutes);
|
||||
numSeconds > 9 ? strings.push('' + numSeconds) : strings.push('0' + numSeconds);
|
||||
return strings.join(':');
|
||||
}
|
||||
|
||||
export function toTimeTicks(size: number, decimals: number, scaledDecimals: number) {
|
||||
return toSeconds(size, decimals, scaledDecimals);
|
||||
}
|
||||
|
||||
export function toClockMilliseconds(size: number, decimals: number) {
|
||||
return toClock(size, decimals);
|
||||
}
|
||||
|
||||
export function toClockSeconds(size: number, decimals: number) {
|
||||
return toClock(size * 1000, decimals);
|
||||
}
|
||||
|
||||
export function dateTimeAsIso(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) {
|
||||
const time = isUtc ? moment.utc(value) : moment(value);
|
||||
|
||||
if (moment().isSame(value, 'day')) {
|
||||
return time.format('HH:mm:ss');
|
||||
}
|
||||
return time.format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
export function dateTimeAsUS(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) {
|
||||
const time = isUtc ? moment.utc(value) : moment(value);
|
||||
|
||||
if (moment().isSame(value, 'day')) {
|
||||
return time.format('h:mm:ss a');
|
||||
}
|
||||
return time.format('MM/DD/YYYY h:mm:ss a');
|
||||
}
|
||||
|
||||
export function dateTimeFromNow(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) {
|
||||
const time = isUtc ? moment.utc(value) : moment(value);
|
||||
return time.fromNow();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { currency } from './symbolFormatters';
|
||||
|
||||
describe('Currency', () => {
|
||||
it('should format as usd', () => {
|
||||
expect(currency('$')(1532.82, 1, -1)).toEqual('$1.53K');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { scaledUnits } from './valueFormats';
|
||||
|
||||
export function currency(symbol: string) {
|
||||
const units = ['', 'K', 'M', 'B', 'T'];
|
||||
const scaler = scaledUnits(1000, units);
|
||||
return (size: number, decimals: number, scaledDecimals: number) => {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
const scaled = scaler(size, decimals, scaledDecimals);
|
||||
return symbol + scaled;
|
||||
};
|
||||
}
|
||||
|
||||
export function binarySIPrefix(unit: string, offset = 0) {
|
||||
const prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice(offset);
|
||||
const units = prefixes.map(p => {
|
||||
return ' ' + p + unit;
|
||||
});
|
||||
return scaledUnits(1024, units);
|
||||
}
|
||||
|
||||
export function decimalSIPrefix(unit: string, offset = 0) {
|
||||
let prefixes = ['n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
|
||||
prefixes = prefixes.slice(3 + (offset || 0));
|
||||
const units = prefixes.map(p => {
|
||||
return ' ' + p + unit;
|
||||
});
|
||||
return scaledUnits(1000, units);
|
||||
}
|
||||
166
packages/grafana-ui/src/utils/valueFormats/valueFormats.ts
Normal file
166
packages/grafana-ui/src/utils/valueFormats/valueFormats.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { getCategories } from './categories';
|
||||
|
||||
type ValueFormatter = (value: number, decimals?: number, scaledDecimals?: number, isUtc?: boolean) => string;
|
||||
|
||||
interface ValueFormat {
|
||||
name: string;
|
||||
id: string;
|
||||
fn: ValueFormatter;
|
||||
}
|
||||
|
||||
export interface ValueFormatCategory {
|
||||
name: string;
|
||||
formats: ValueFormat[];
|
||||
}
|
||||
|
||||
interface ValueFormatterIndex {
|
||||
[id: string]: ValueFormatter;
|
||||
}
|
||||
|
||||
// Globals & formats cache
|
||||
let categories: ValueFormatCategory[] = [];
|
||||
const index: ValueFormatterIndex = {};
|
||||
let hasBuiltIndex = false;
|
||||
|
||||
export function toFixed(value: number, decimals?: number): string {
|
||||
if (value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const factor = decimals ? Math.pow(10, Math.max(0, decimals)) : 1;
|
||||
const formatted = String(Math.round(value * factor) / factor);
|
||||
|
||||
// if exponent return directly
|
||||
if (formatted.indexOf('e') !== -1 || value === 0) {
|
||||
return formatted;
|
||||
}
|
||||
|
||||
// If tickDecimals was specified, ensure that we have exactly that
|
||||
// much precision; otherwise default to the value's own precision.
|
||||
if (decimals != null) {
|
||||
const decimalPos = formatted.indexOf('.');
|
||||
const precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1;
|
||||
if (precision < decimals) {
|
||||
return (precision ? formatted : formatted + '.') + String(factor).substr(1, decimals - precision);
|
||||
}
|
||||
}
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
export function toFixedScaled(
|
||||
value: number,
|
||||
decimals: number,
|
||||
scaledDecimals: number,
|
||||
additionalDecimals: number,
|
||||
ext: string
|
||||
) {
|
||||
if (scaledDecimals === null) {
|
||||
return toFixed(value, decimals) + ext;
|
||||
} else {
|
||||
return toFixed(value, scaledDecimals + additionalDecimals) + ext;
|
||||
}
|
||||
}
|
||||
|
||||
export function toFixedUnit(unit: string) {
|
||||
return (size: number, decimals: number) => {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
return toFixed(size, decimals) + ' ' + unit;
|
||||
};
|
||||
}
|
||||
|
||||
// Formatter which scales the unit string geometrically according to the given
|
||||
// numeric factor. Repeatedly scales the value down by the factor until it is
|
||||
// less than the factor in magnitude, or the end of the array is reached.
|
||||
export function scaledUnits(factor: number, extArray: string[]) {
|
||||
return (size: number, decimals: number, scaledDecimals: number) => {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let steps = 0;
|
||||
const limit = extArray.length;
|
||||
|
||||
while (Math.abs(size) >= factor) {
|
||||
steps++;
|
||||
size /= factor;
|
||||
|
||||
if (steps >= limit) {
|
||||
return 'NA';
|
||||
}
|
||||
}
|
||||
|
||||
if (steps > 0 && scaledDecimals !== null) {
|
||||
decimals = scaledDecimals + 3 * steps;
|
||||
}
|
||||
|
||||
return toFixed(size, decimals) + extArray[steps];
|
||||
};
|
||||
}
|
||||
|
||||
export function locale(value: number, decimals: number) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
return value.toLocaleString(undefined, { maximumFractionDigits: decimals });
|
||||
}
|
||||
|
||||
export function simpleCountUnit(symbol: string) {
|
||||
const units = ['', 'K', 'M', 'B', 'T'];
|
||||
const scaler = scaledUnits(1000, units);
|
||||
return (size: number, decimals: number, scaledDecimals: number) => {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
const scaled = scaler(size, decimals, scaledDecimals);
|
||||
return scaled + ' ' + symbol;
|
||||
};
|
||||
}
|
||||
|
||||
function buildFormats() {
|
||||
categories = getCategories();
|
||||
|
||||
for (const cat of categories) {
|
||||
for (const format of cat.formats) {
|
||||
index[format.id] = format.fn;
|
||||
}
|
||||
}
|
||||
|
||||
hasBuiltIndex = true;
|
||||
}
|
||||
|
||||
export function getValueFormat(id: string): ValueFormatter {
|
||||
if (!hasBuiltIndex) {
|
||||
buildFormats();
|
||||
}
|
||||
|
||||
return index[id];
|
||||
}
|
||||
|
||||
export function getValueFormatterIndex(): ValueFormatterIndex {
|
||||
if (!hasBuiltIndex) {
|
||||
buildFormats();
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
export function getValueFormats() {
|
||||
if (!hasBuiltIndex) {
|
||||
buildFormats();
|
||||
}
|
||||
|
||||
return categories.map(cat => {
|
||||
return {
|
||||
text: cat.name,
|
||||
submenu: cat.formats.map(format => {
|
||||
return {
|
||||
text: format.name,
|
||||
value: format.id,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { Graph } from './Graph/Graph';
|
||||
Reference in New Issue
Block a user