mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'grafana/master' into table-reducer
* grafana/master: (101 commits) refactor: merged types and updated references Remove leftover from first iteration Snapshot update fix: ts issue on SelectOption test chore: Bump react and react-dom to 16.8.4 Update latest.json Update templating.md Update CHANGELOG.md chore: cleaning up noimplicit anys in search_srv and tests progress: #14714 Fix threshold editor color picker not working for custom colors Updated threshold editor test Re-render gauge / singlestat panels when changing options fix: refactored so members are loaded by TeamPages and use hideFromTabs instead of filtering out children in navModel teams: explains the external property of a team membership. fix: fixed snapshots and permission select not beeing able to click fix: new team link goes nowhere for viewers teams: refactor so that you can only delete teams if you are team admin permissions: removes global access to bus from MakeUserAdmin. teams: local access to bus, moving away from dep on global. teams: better names for api permissions. ...
This commit is contained in:
@@ -25,10 +25,10 @@
|
||||
"lodash": "^4.17.10",
|
||||
"moment": "^2.22.2",
|
||||
"papaparse": "^4.6.3",
|
||||
"react": "^16.6.3",
|
||||
"react": "^16.8.4",
|
||||
"react-color": "^2.17.0",
|
||||
"react-custom-scrollbars": "^4.2.1",
|
||||
"react-dom": "^16.6.3",
|
||||
"react-dom": "^16.8.4",
|
||||
"react-highlight-words": "0.11.0",
|
||||
"react-popper": "^1.3.0",
|
||||
"react-transition-group": "^2.2.1",
|
||||
@@ -48,7 +48,7 @@
|
||||
"@types/lodash": "^4.14.119",
|
||||
"@types/node": "^10.12.18",
|
||||
"@types/papaparse": "^4.5.9",
|
||||
"@types/react": "^16.7.6",
|
||||
"@types/react": "^16.8.8",
|
||||
"@types/react-custom-scrollbars": "^4.0.5",
|
||||
"@types/react-test-renderer": "^16.0.3",
|
||||
"@types/react-transition-group": "^2.0.15",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { number, text } from '@storybook/addon-knobs';
|
||||
import { number, text, boolean } from '@storybook/addon-knobs';
|
||||
import { BarGauge } from './BarGauge';
|
||||
import { VizOrientation } from '../../types';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
|
||||
|
||||
@@ -15,6 +16,8 @@ const getKnobs = () => {
|
||||
threshold2Color: text('threshold2Color', 'red'),
|
||||
unit: text('unit', 'ms'),
|
||||
decimals: number('decimals', 1),
|
||||
horizontal: boolean('horizontal', false),
|
||||
lcd: boolean('lcd', false),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -22,7 +25,7 @@ const BarGaugeStories = storiesOf('UI/BarGauge/BarGauge', module);
|
||||
|
||||
BarGaugeStories.addDecorator(withCenteredStory);
|
||||
|
||||
BarGaugeStories.add('Vertical, with basic thresholds', () => {
|
||||
BarGaugeStories.add('Simple with basic thresholds', () => {
|
||||
const {
|
||||
value,
|
||||
minValue,
|
||||
@@ -33,11 +36,13 @@ BarGaugeStories.add('Vertical, with basic thresholds', () => {
|
||||
threshold2Value,
|
||||
unit,
|
||||
decimals,
|
||||
horizontal,
|
||||
lcd,
|
||||
} = getKnobs();
|
||||
|
||||
return renderComponentWithTheme(BarGauge, {
|
||||
width: 200,
|
||||
height: 400,
|
||||
width: 700,
|
||||
height: 700,
|
||||
value: value,
|
||||
minValue: minValue,
|
||||
maxValue: maxValue,
|
||||
@@ -45,6 +50,8 @@ BarGaugeStories.add('Vertical, with basic thresholds', () => {
|
||||
prefix: '',
|
||||
postfix: '',
|
||||
decimals: decimals,
|
||||
orientation: horizontal ? VizOrientation.Horizontal : VizOrientation.Vertical,
|
||||
displayMode: lcd ? 'lcd' : 'simple',
|
||||
thresholds: [
|
||||
{ index: 0, value: -Infinity, color: 'green' },
|
||||
{ index: 1, value: threshold1Value, color: threshold1Color },
|
||||
|
||||
@@ -12,6 +12,7 @@ const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
maxValue: 100,
|
||||
minValue: 0,
|
||||
displayMode: 'basic',
|
||||
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
|
||||
height: 300,
|
||||
width: 300,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Library
|
||||
import React, { PureComponent, CSSProperties } from 'react';
|
||||
import React, { PureComponent, CSSProperties, ReactNode } from 'react';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
// Utils
|
||||
@@ -18,11 +18,9 @@ export interface Props extends Themeable {
|
||||
maxValue: number;
|
||||
minValue: number;
|
||||
orientation: VizOrientation;
|
||||
displayMode: 'basic' | 'lcd' | 'gradient';
|
||||
}
|
||||
|
||||
/*
|
||||
* This visualization is still in POC state, needed more tests & better structure
|
||||
*/
|
||||
export class BarGauge extends PureComponent<Props> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
maxValue: 100,
|
||||
@@ -31,10 +29,22 @@ export class BarGauge extends PureComponent<Props> {
|
||||
text: '100',
|
||||
numeric: 100,
|
||||
},
|
||||
displayMode: 'lcd',
|
||||
orientation: VizOrientation.Horizontal,
|
||||
thresholds: [],
|
||||
};
|
||||
|
||||
render() {
|
||||
switch (this.props.displayMode) {
|
||||
case 'lcd':
|
||||
return this.renderRetroBars();
|
||||
case 'basic':
|
||||
case 'gradient':
|
||||
default:
|
||||
return this.renderBasicAndGradientBars();
|
||||
}
|
||||
}
|
||||
|
||||
getValueColors(): BarColors {
|
||||
const { thresholds, theme, value } = this.props;
|
||||
|
||||
@@ -46,41 +56,19 @@ export class BarGauge extends PureComponent<Props> {
|
||||
return {
|
||||
value: color,
|
||||
border: color,
|
||||
bar: tinycolor(color)
|
||||
.setAlpha(0.3)
|
||||
background: tinycolor(color)
|
||||
.setAlpha(0.15)
|
||||
.toRgbString(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: getColorFromHexRgbOrName('gray', theme.type),
|
||||
bar: getColorFromHexRgbOrName('gray', theme.type),
|
||||
background: getColorFromHexRgbOrName('gray', theme.type),
|
||||
border: getColorFromHexRgbOrName('gray', theme.type),
|
||||
};
|
||||
}
|
||||
|
||||
getCellColor(positionValue: TimeSeriesValue): string {
|
||||
const { thresholds, theme, value } = this.props;
|
||||
const activeThreshold = getThresholdForValue(thresholds, positionValue);
|
||||
|
||||
if (activeThreshold !== null) {
|
||||
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
|
||||
|
||||
// if we are past real value the cell is not "on"
|
||||
if (value === null || (positionValue !== null && positionValue > value.numeric)) {
|
||||
return tinycolor(color)
|
||||
.setAlpha(0.15)
|
||||
.toRgbString();
|
||||
} else {
|
||||
return tinycolor(color)
|
||||
.setAlpha(0.7)
|
||||
.toRgbString();
|
||||
}
|
||||
}
|
||||
|
||||
return 'gray';
|
||||
}
|
||||
|
||||
getValueStyles(value: string, color: string, width: number): CSSProperties {
|
||||
const guess = width / (value.length * 1.1);
|
||||
const fontSize = Math.min(Math.max(guess, 14), 40);
|
||||
@@ -91,107 +79,205 @@ export class BarGauge extends PureComponent<Props> {
|
||||
};
|
||||
}
|
||||
|
||||
renderVerticalBar(valueFormatted: string, valuePercent: number) {
|
||||
/*
|
||||
* Return width or height depending on viz orientation
|
||||
* */
|
||||
get size() {
|
||||
const { height, width } = this.props;
|
||||
return this.isVertical ? height : width;
|
||||
}
|
||||
|
||||
const maxHeight = height * BAR_SIZE_RATIO;
|
||||
const barHeight = Math.max(valuePercent * maxHeight, 0);
|
||||
get isVertical() {
|
||||
return this.props.orientation === VizOrientation.Vertical;
|
||||
}
|
||||
|
||||
getBarGradient(maxSize: number): string {
|
||||
const { minValue, maxValue, thresholds, value } = this.props;
|
||||
const cssDirection = this.isVertical ? '0deg' : '90deg';
|
||||
|
||||
let gradient = '';
|
||||
let lastpos = 0;
|
||||
|
||||
for (let i = 0; i < thresholds.length; i++) {
|
||||
const threshold = thresholds[i];
|
||||
const color = getColorFromHexRgbOrName(threshold.color);
|
||||
const valuePercent = Math.min(threshold.value / (maxValue - minValue), 1);
|
||||
const pos = valuePercent * maxSize;
|
||||
const offset = Math.round(pos - (pos - lastpos) / 2);
|
||||
|
||||
if (gradient === '') {
|
||||
gradient = `linear-gradient(${cssDirection}, ${color}, ${color}`;
|
||||
} else if (value.numeric < threshold.value) {
|
||||
break;
|
||||
} else {
|
||||
lastpos = pos;
|
||||
gradient += ` ${offset}px, ${color}`;
|
||||
}
|
||||
}
|
||||
|
||||
return gradient + ')';
|
||||
}
|
||||
|
||||
renderBasicAndGradientBars(): ReactNode {
|
||||
const { height, width, displayMode, maxValue, minValue, value } = this.props;
|
||||
|
||||
const valuePercent = Math.min(value.numeric / (maxValue - minValue), 1);
|
||||
const maxSize = this.size * BAR_SIZE_RATIO;
|
||||
const barSize = Math.max(valuePercent * maxSize, 0);
|
||||
const colors = this.getValueColors();
|
||||
const valueStyles = this.getValueStyles(valueFormatted, colors.value, width);
|
||||
const spaceForText = this.isVertical ? width : Math.min(this.size - maxSize, height);
|
||||
const valueStyles = this.getValueStyles(value.text, colors.value, spaceForText);
|
||||
const isBasic = displayMode === 'basic';
|
||||
|
||||
const containerStyles: CSSProperties = {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
};
|
||||
|
||||
const barStyles: CSSProperties = {
|
||||
height: `${barHeight}px`,
|
||||
width: `${width}px`,
|
||||
backgroundColor: colors.bar,
|
||||
borderTop: `1px solid ${colors.border}`,
|
||||
borderRadius: '3px',
|
||||
};
|
||||
|
||||
if (this.isVertical) {
|
||||
// Custom styles for vertical orientation
|
||||
containerStyles.flexDirection = 'column';
|
||||
containerStyles.justifyContent = 'flex-end';
|
||||
barStyles.transition = 'height 1s';
|
||||
barStyles.height = `${barSize}px`;
|
||||
barStyles.width = `${width}px`;
|
||||
if (isBasic) {
|
||||
// Basic styles
|
||||
barStyles.background = `${colors.background}`;
|
||||
barStyles.border = `1px solid ${colors.border}`;
|
||||
barStyles.boxShadow = `0 0 4px ${colors.border}`;
|
||||
} else {
|
||||
// Gradient styles
|
||||
barStyles.background = this.getBarGradient(maxSize);
|
||||
}
|
||||
} else {
|
||||
// Custom styles for horizontal orientation
|
||||
containerStyles.flexDirection = 'row-reverse';
|
||||
containerStyles.justifyContent = 'flex-end';
|
||||
containerStyles.alignItems = 'center';
|
||||
barStyles.transition = 'width 1s';
|
||||
barStyles.height = `${height}px`;
|
||||
barStyles.width = `${barSize}px`;
|
||||
barStyles.marginRight = '10px';
|
||||
|
||||
if (isBasic) {
|
||||
// Basic styles
|
||||
barStyles.background = `${colors.background}`;
|
||||
barStyles.border = `1px solid ${colors.border}`;
|
||||
barStyles.boxShadow = `0 0 4px ${colors.border}`;
|
||||
} else {
|
||||
// Gradient styles
|
||||
barStyles.background = this.getBarGradient(maxSize);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={containerStyles}>
|
||||
<div className="bar-gauge__value" style={valueStyles}>
|
||||
{valueFormatted}
|
||||
{value.text}
|
||||
</div>
|
||||
<div style={barStyles} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderHorizontalBar(valueFormatted: string, valuePercent: number) {
|
||||
const { height, width } = this.props;
|
||||
getCellColor(positionValue: TimeSeriesValue): CellColors {
|
||||
const { thresholds, theme, value } = this.props;
|
||||
const activeThreshold = getThresholdForValue(thresholds, positionValue);
|
||||
|
||||
const maxWidth = width * BAR_SIZE_RATIO;
|
||||
const barWidth = Math.max(valuePercent * maxWidth, 0);
|
||||
const colors = this.getValueColors();
|
||||
const valueStyles = this.getValueStyles(valueFormatted, colors.value, width * (1 - BAR_SIZE_RATIO));
|
||||
if (activeThreshold !== null) {
|
||||
const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
|
||||
|
||||
valueStyles.marginLeft = '8px';
|
||||
// if we are past real value the cell is not "on"
|
||||
if (value === null || (positionValue !== null && positionValue > value.numeric)) {
|
||||
return {
|
||||
background: tinycolor(color)
|
||||
.setAlpha(0.15)
|
||||
.toRgbString(),
|
||||
border: 'transparent',
|
||||
isLit: false,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
background: tinycolor(color)
|
||||
.setAlpha(0.85)
|
||||
.toRgbString(),
|
||||
backgroundShade: tinycolor(color)
|
||||
.setAlpha(0.55)
|
||||
.toRgbString(),
|
||||
border: tinycolor(color)
|
||||
.setAlpha(0.9)
|
||||
.toRgbString(),
|
||||
isLit: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const containerStyles: CSSProperties = {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
return {
|
||||
background: 'gray',
|
||||
border: 'gray',
|
||||
};
|
||||
|
||||
const barStyles = {
|
||||
height: `${height}px`,
|
||||
width: `${barWidth}px`,
|
||||
backgroundColor: colors.bar,
|
||||
borderRight: `1px solid ${colors.border}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyles}>
|
||||
<div style={barStyles} />
|
||||
<div className="bar-gauge__value" style={valueStyles}>
|
||||
{valueFormatted}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderHorizontalLCD(valueFormatted: string, valuePercent: number) {
|
||||
const { height, width, maxValue, minValue } = this.props;
|
||||
renderRetroBars(): ReactNode {
|
||||
const { height, width, maxValue, minValue, value } = this.props;
|
||||
|
||||
const valueRange = maxValue - minValue;
|
||||
const maxWidth = width * BAR_SIZE_RATIO;
|
||||
const cellSpacing = 4;
|
||||
const cellCount = 30;
|
||||
const cellWidth = (maxWidth - cellSpacing * cellCount) / cellCount;
|
||||
const maxSize = this.size * BAR_SIZE_RATIO;
|
||||
const cellSpacing = 5;
|
||||
const cellCount = maxSize / 20;
|
||||
const cellSize = (maxSize - cellSpacing * cellCount) / cellCount;
|
||||
const colors = this.getValueColors();
|
||||
const valueStyles = this.getValueStyles(valueFormatted, colors.value, width * (1 - BAR_SIZE_RATIO));
|
||||
valueStyles.marginLeft = '8px';
|
||||
const spaceForText = this.isVertical ? width : Math.min(this.size - maxSize, height);
|
||||
const valueStyles = this.getValueStyles(value.text, colors.value, spaceForText);
|
||||
|
||||
const containerStyles: CSSProperties = {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
if (this.isVertical) {
|
||||
containerStyles.flexDirection = 'column-reverse';
|
||||
containerStyles.alignItems = 'center';
|
||||
valueStyles.marginBottom = '20px';
|
||||
} else {
|
||||
containerStyles.flexDirection = 'row';
|
||||
containerStyles.alignItems = 'center';
|
||||
valueStyles.marginLeft = '20px';
|
||||
}
|
||||
|
||||
const cells: JSX.Element[] = [];
|
||||
|
||||
for (let i = 0; i < cellCount; i++) {
|
||||
const currentValue = (valueRange / cellCount) * i;
|
||||
const cellColor = this.getCellColor(currentValue);
|
||||
const cellStyles: CSSProperties = {
|
||||
width: `${cellWidth}px`,
|
||||
backgroundColor: cellColor,
|
||||
marginRight: '4px',
|
||||
height: `${height}px`,
|
||||
borderRadius: '2px',
|
||||
};
|
||||
|
||||
if (cellColor.isLit) {
|
||||
cellStyles.boxShadow = `0 0 4px ${cellColor.border}`;
|
||||
cellStyles.backgroundImage = `radial-gradient(${cellColor.background} 10%, ${cellColor.backgroundShade})`;
|
||||
} else {
|
||||
cellStyles.backgroundColor = cellColor.background;
|
||||
}
|
||||
|
||||
if (this.isVertical) {
|
||||
cellStyles.height = `${cellSize}px`;
|
||||
cellStyles.width = `${width}px`;
|
||||
cellStyles.marginTop = `${cellSpacing}px`;
|
||||
} else {
|
||||
cellStyles.width = `${cellSize}px`;
|
||||
cellStyles.height = `${height}px`;
|
||||
cellStyles.marginRight = `${cellSpacing}px`;
|
||||
}
|
||||
|
||||
cells.push(<div style={cellStyles} />);
|
||||
}
|
||||
|
||||
@@ -199,26 +285,22 @@ export class BarGauge extends PureComponent<Props> {
|
||||
<div style={containerStyles}>
|
||||
{cells}
|
||||
<div className="bar-gauge__value" style={valueStyles}>
|
||||
{valueFormatted}
|
||||
{value.text}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { maxValue, minValue, orientation, value } = this.props;
|
||||
|
||||
const valuePercent = Math.min(value.numeric / (maxValue - minValue), 1);
|
||||
const vertical = orientation === 'vertical';
|
||||
|
||||
return vertical
|
||||
? this.renderVerticalBar(value.text, valuePercent)
|
||||
: this.renderHorizontalLCD(value.text, valuePercent);
|
||||
}
|
||||
}
|
||||
|
||||
interface BarColors {
|
||||
value: string;
|
||||
bar: string;
|
||||
background: string;
|
||||
border: string;
|
||||
}
|
||||
|
||||
interface CellColors {
|
||||
background: string;
|
||||
backgroundShade?: string;
|
||||
border: string;
|
||||
isLit?: boolean;
|
||||
}
|
||||
|
||||
@@ -6,353 +6,37 @@ exports[`Render BarGauge with basic options should render 1`] = `
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"display": "flex",
|
||||
"flexDirection": "row",
|
||||
"flexDirection": "row-reverse",
|
||||
"height": "300px",
|
||||
"justifyContent": "flex-end",
|
||||
"width": "300px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.7)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(126, 178, 109, 0.15)",
|
||||
"borderRadius": "2px",
|
||||
"height": "300px",
|
||||
"marginRight": "4px",
|
||||
"width": "4px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="bar-gauge__value"
|
||||
style={
|
||||
Object {
|
||||
"color": "#7EB26D",
|
||||
"fontSize": "27.272727272727263px",
|
||||
"marginLeft": "8px",
|
||||
"fontSize": "27.27272727272727px",
|
||||
}
|
||||
}
|
||||
>
|
||||
25
|
||||
</div>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"background": "rgba(126, 178, 109, 0.15)",
|
||||
"border": "1px solid #7EB26D",
|
||||
"borderRadius": "3px",
|
||||
"boxShadow": "0 0 4px #7EB26D",
|
||||
"height": "300px",
|
||||
"marginRight": "10px",
|
||||
"transition": "width 1s",
|
||||
"width": "60px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { PureComponent, SyntheticEvent } from 'react';
|
||||
|
||||
interface Props {
|
||||
onConfirm(): void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -33,25 +34,22 @@ export class DeleteButton extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { onConfirm } = this.props;
|
||||
let showConfirm;
|
||||
let showDeleteButton;
|
||||
|
||||
if (this.state.showConfirm) {
|
||||
showConfirm = 'show';
|
||||
showDeleteButton = 'hide';
|
||||
} else {
|
||||
showConfirm = 'hide';
|
||||
showDeleteButton = 'show';
|
||||
}
|
||||
const { onConfirm, disabled } = this.props;
|
||||
const showConfirmClass = this.state.showConfirm ? 'show' : 'hide';
|
||||
const showDeleteButtonClass = this.state.showConfirm ? 'hide' : 'show';
|
||||
const disabledClass = disabled ? 'disabled btn-inverse' : '';
|
||||
const onClick = disabled ? () => {} : this.onClickDelete;
|
||||
|
||||
return (
|
||||
<span className="delete-button-container">
|
||||
<a className={'delete-button ' + showDeleteButton + ' btn btn-danger btn-small'} onClick={this.onClickDelete}>
|
||||
<a
|
||||
className={`delete-button ${showDeleteButtonClass} btn btn-danger btn-small ${disabledClass}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<i className="fa fa-remove" />
|
||||
</a>
|
||||
<span className="confirm-delete-container">
|
||||
<span className={'confirm-delete ' + showConfirm}>
|
||||
<span className={`confirm-delete ${showConfirmClass}`}>
|
||||
<a className="btn btn-small" onClick={this.onClickCancel}>
|
||||
Cancel
|
||||
</a>
|
||||
|
||||
51
packages/grafana-ui/src/components/Input/Input.test.tsx
Normal file
51
packages/grafana-ui/src/components/Input/Input.test.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Input } from './Input';
|
||||
import { EventsWithValidation } from '../../utils';
|
||||
import { ValidationEvents } from '../../types';
|
||||
|
||||
const TEST_ERROR_MESSAGE = 'Value must be empty or less than 3 chars';
|
||||
const testBlurValidation: ValidationEvents = {
|
||||
[EventsWithValidation.onBlur]: [
|
||||
{
|
||||
rule: (value: string) => {
|
||||
return !value || value.length < 3;
|
||||
},
|
||||
errorMessage: TEST_ERROR_MESSAGE,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Input', () => {
|
||||
it('renders correctly', () => {
|
||||
const tree = renderer.create(<Input />).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should validate with error onBlur', () => {
|
||||
const wrapper = shallow(<Input validationEvents={testBlurValidation} />);
|
||||
const evt = {
|
||||
persist: jest.fn,
|
||||
target: {
|
||||
value: 'I can not be more than 2 chars',
|
||||
},
|
||||
};
|
||||
|
||||
wrapper.find('input').simulate('blur', evt);
|
||||
expect(wrapper.state('error')).toBe(TEST_ERROR_MESSAGE);
|
||||
});
|
||||
|
||||
it('should validate without error onBlur', () => {
|
||||
const wrapper = shallow(<Input validationEvents={testBlurValidation} />);
|
||||
const evt = {
|
||||
persist: jest.fn,
|
||||
target: {
|
||||
value: 'Hi',
|
||||
},
|
||||
};
|
||||
|
||||
wrapper.find('input').simulate('blur', evt);
|
||||
expect(wrapper.state('error')).toBe(null);
|
||||
});
|
||||
});
|
||||
81
packages/grafana-ui/src/components/Input/Input.tsx
Normal file
81
packages/grafana-ui/src/components/Input/Input.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React, { PureComponent, ChangeEvent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { validate, EventsWithValidation, hasValidationEvent } from '../../utils';
|
||||
import { ValidationEvents, ValidationRule } from '../../types';
|
||||
|
||||
export enum InputStatus {
|
||||
Invalid = 'invalid',
|
||||
Valid = 'valid',
|
||||
}
|
||||
|
||||
interface Props extends React.HTMLProps<HTMLInputElement> {
|
||||
validationEvents?: ValidationEvents;
|
||||
hideErrorMessage?: boolean;
|
||||
|
||||
// Override event props and append status as argument
|
||||
onBlur?: (event: React.FocusEvent<HTMLInputElement>, status?: InputStatus) => void;
|
||||
onFocus?: (event: React.FocusEvent<HTMLInputElement>, status?: InputStatus) => void;
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>, status?: InputStatus) => void;
|
||||
}
|
||||
|
||||
export class Input extends PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
className: '',
|
||||
};
|
||||
|
||||
state = {
|
||||
error: null,
|
||||
};
|
||||
|
||||
get status() {
|
||||
return this.state.error ? InputStatus.Invalid : InputStatus.Valid;
|
||||
}
|
||||
|
||||
get isInvalid() {
|
||||
return this.status === InputStatus.Invalid;
|
||||
}
|
||||
|
||||
validatorAsync = (validationRules: ValidationRule[]) => {
|
||||
return (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const errors = validate(evt.target.value, validationRules);
|
||||
this.setState(prevState => {
|
||||
return { ...prevState, error: errors ? errors[0] : null };
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
populateEventPropsWithStatus = (restProps: any, validationEvents: ValidationEvents | undefined) => {
|
||||
const inputElementProps = { ...restProps };
|
||||
if (!validationEvents) {
|
||||
return inputElementProps;
|
||||
}
|
||||
Object.keys(EventsWithValidation).forEach(eventName => {
|
||||
if (hasValidationEvent(eventName as EventsWithValidation, validationEvents) || restProps[eventName]) {
|
||||
inputElementProps[eventName] = async (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
evt.persist(); // Needed for async. https://reactjs.org/docs/events.html#event-pooling
|
||||
if (hasValidationEvent(eventName as EventsWithValidation, validationEvents)) {
|
||||
await this.validatorAsync(validationEvents[eventName]).apply(this, [evt]);
|
||||
}
|
||||
if (restProps[eventName]) {
|
||||
restProps[eventName].apply(null, [evt, this.status]);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
return inputElementProps;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { validationEvents, className, hideErrorMessage, ...restProps } = this.props;
|
||||
const { error } = this.state;
|
||||
const inputClassName = classNames('gf-form-input', { invalid: this.isInvalid }, className);
|
||||
const inputElementProps = this.populateEventPropsWithStatus(restProps, validationEvents);
|
||||
|
||||
return (
|
||||
<div className="our-custom-wrapper-class">
|
||||
<input {...inputElementProps} className={inputClassName} />
|
||||
{error && !hideErrorMessage && <span>{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Input renders correctly 1`] = `
|
||||
<div
|
||||
className="our-custom-wrapper-class"
|
||||
>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -25,6 +25,7 @@ const model: OptionProps<any> = {
|
||||
key: '',
|
||||
onClick: jest.fn(),
|
||||
onMouseOver: jest.fn(),
|
||||
onMouseMove: jest.fn(),
|
||||
tabIndex: 1,
|
||||
},
|
||||
label: 'Option label',
|
||||
|
||||
@@ -4,6 +4,7 @@ exports[`SelectOption renders correctly 1`] = `
|
||||
<div
|
||||
id=""
|
||||
onClick={[MockFunction]}
|
||||
onMouseMove={[MockFunction]}
|
||||
onMouseOver={[MockFunction]}
|
||||
tabIndex={1}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { ThresholdsEditor } from './ThresholdsEditor';
|
||||
|
||||
const ThresholdsEditorStories = storiesOf('UI/ThresholdsEditor', module);
|
||||
const thresholds = [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 50, color: 'red' }];
|
||||
|
||||
ThresholdsEditorStories.add('default', () => {
|
||||
return <ThresholdsEditor thresholds={[]} onChange={action('Thresholds changed')} />;
|
||||
});
|
||||
|
||||
ThresholdsEditorStories.add('with thresholds', () => {
|
||||
return <ThresholdsEditor thresholds={thresholds} onChange={action('Thresholds changed')} />;
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { ThresholdsEditor, Props } from './ThresholdsEditor';
|
||||
import { colors } from '../../utils';
|
||||
|
||||
const setup = (propOverrides?: Partial<Props>) => {
|
||||
const props: Props = {
|
||||
@@ -31,7 +32,7 @@ describe('Initialization', () => {
|
||||
it('should add a base threshold if missing', () => {
|
||||
const { instance } = setup();
|
||||
|
||||
expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]);
|
||||
expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: colors[0] }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,7 +42,7 @@ describe('Add threshold', () => {
|
||||
|
||||
instance.onAddThreshold(0);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]);
|
||||
expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: colors[0] }]);
|
||||
});
|
||||
|
||||
it('should add threshold', () => {
|
||||
@@ -50,41 +51,41 @@ describe('Add threshold', () => {
|
||||
instance.onAddThreshold(1);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 0, value: -Infinity, color: colors[0] },
|
||||
{ index: 1, value: 50, color: colors[2] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should add another threshold above a first', () => {
|
||||
const { instance } = setup({
|
||||
thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }],
|
||||
thresholds: [{ index: 0, value: -Infinity, color: colors[0] }, { index: 1, value: 50, color: colors[2] }],
|
||||
});
|
||||
|
||||
instance.onAddThreshold(2);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
{ index: 0, value: -Infinity, color: colors[0] },
|
||||
{ index: 1, value: 50, color: colors[2] },
|
||||
{ index: 2, value: 75, color: colors[3] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should add another threshold between first and second index', () => {
|
||||
const { instance } = setup({
|
||||
thresholds: [
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 2, value: 75, color: '#6ED0E0' },
|
||||
{ index: 0, value: -Infinity, color: colors[0] },
|
||||
{ index: 1, value: 50, color: colors[2] },
|
||||
{ index: 2, value: 75, color: colors[3] },
|
||||
],
|
||||
});
|
||||
|
||||
instance.onAddThreshold(2);
|
||||
|
||||
expect(instance.state.thresholds).toEqual([
|
||||
{ index: 0, value: -Infinity, color: '#7EB26D' },
|
||||
{ index: 1, value: 50, color: '#EAB839' },
|
||||
{ index: 2, value: 62.5, color: '#EF843C' },
|
||||
{ index: 3, value: 75, color: '#6ED0E0' },
|
||||
{ index: 0, value: -Infinity, color: colors[0] },
|
||||
{ index: 1, value: 50, color: colors[2] },
|
||||
{ index: 2, value: 62.5, color: colors[4] },
|
||||
{ index: 3, value: 75, color: colors[3] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Threshold } from '../../types';
|
||||
import { ColorPicker } from '..';
|
||||
import { PanelOptionsGroup } from '..';
|
||||
import { colors } from '../../utils';
|
||||
import { ThemeContext } from '../../themes/ThemeContext';
|
||||
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
|
||||
import { ThemeContext } from '../../themes';
|
||||
import { getColorFromHexRgbOrName } from '../../utils';
|
||||
|
||||
export interface Props {
|
||||
thresholds: Threshold[];
|
||||
@@ -166,7 +166,11 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
|
||||
<div className="thresholds-row-input-inner-color">
|
||||
{threshold.color && (
|
||||
<div className="thresholds-row-input-inner-color-colorpicker">
|
||||
<ColorPicker color={threshold.color} onChange={color => this.onChangeThresholdColor(threshold, color)} />
|
||||
<ColorPicker
|
||||
color={threshold.color}
|
||||
onChange={color => this.onChangeThresholdColor(threshold, color)}
|
||||
enableNamedColors={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,450 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render with base threshold 1`] = `
|
||||
<ContextConsumer>
|
||||
<Component />
|
||||
</ContextConsumer>
|
||||
<ThresholdsEditor
|
||||
onChange={
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"color": "#7EB26D",
|
||||
"index": 0,
|
||||
"value": -Infinity,
|
||||
},
|
||||
],
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"isThrow": false,
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
thresholds={Array []}
|
||||
>
|
||||
<Component
|
||||
title="Thresholds"
|
||||
>
|
||||
<div
|
||||
className="panel-options-group"
|
||||
>
|
||||
<div
|
||||
className="panel-options-group__header"
|
||||
>
|
||||
<span
|
||||
className="panel-options-group__title"
|
||||
>
|
||||
Thresholds
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="panel-options-group__body"
|
||||
>
|
||||
<div
|
||||
className="thresholds"
|
||||
>
|
||||
<div
|
||||
className="thresholds-row"
|
||||
key="0-0"
|
||||
>
|
||||
<div
|
||||
className="thresholds-row-add-button"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<i
|
||||
className="fa fa-plus"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="thresholds-row-color-indicator"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#7EB26D",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="thresholds-row-input"
|
||||
>
|
||||
<div
|
||||
className="thresholds-row-input-inner"
|
||||
>
|
||||
<span
|
||||
className="thresholds-row-input-inner-arrow"
|
||||
/>
|
||||
<div
|
||||
className="thresholds-row-input-inner-color"
|
||||
>
|
||||
<div
|
||||
className="thresholds-row-input-inner-color-colorpicker"
|
||||
>
|
||||
<WithTheme(ColorPicker)
|
||||
color="#7EB26D"
|
||||
enableNamedColors={true}
|
||||
onChange={[Function]}
|
||||
>
|
||||
<ColorPicker
|
||||
color="#7EB26D"
|
||||
enableNamedColors={true}
|
||||
onChange={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"background": Object {
|
||||
"dropdown": "#1f1f20",
|
||||
"scrollbar": "#343436",
|
||||
"scrollbar2": "#343436",
|
||||
},
|
||||
"border": Object {
|
||||
"radius": Object {
|
||||
"lg": "5px",
|
||||
"md": "3px",
|
||||
"sm": "2px",
|
||||
},
|
||||
"width": Object {
|
||||
"sm": "1px",
|
||||
},
|
||||
},
|
||||
"breakpoints": Object {
|
||||
"lg": "992px",
|
||||
"md": "768px",
|
||||
"sm": "544px",
|
||||
"xl": "1200px",
|
||||
"xs": "0",
|
||||
},
|
||||
"colors": Object {
|
||||
"black": "#000000",
|
||||
"blue": "#33b5e5",
|
||||
"blueBase": "#3274d9",
|
||||
"blueFaint": "#041126",
|
||||
"blueLight": "#5794f2",
|
||||
"blueShade": "#1f60c4",
|
||||
"body": "#d8d9da",
|
||||
"bodyBg": "#161719",
|
||||
"brandDanger": "#e02f44",
|
||||
"brandPrimary": "#eb7b18",
|
||||
"brandSuccess": "#299c46",
|
||||
"brandWarning": "#eb7b18",
|
||||
"critical": "#e02f44",
|
||||
"dark1": "#141414",
|
||||
"dark10": "#424345",
|
||||
"dark2": "#161719",
|
||||
"dark3": "#1f1f20",
|
||||
"dark4": "#212124",
|
||||
"dark5": "#222426",
|
||||
"dark6": "#262628",
|
||||
"dark7": "#292a2d",
|
||||
"dark8": "#2f2f32",
|
||||
"dark9": "#343436",
|
||||
"gray1": "#555555",
|
||||
"gray2": "#8e8e8e",
|
||||
"gray3": "#b3b3b3",
|
||||
"gray4": "#d8d9da",
|
||||
"gray5": "#ececec",
|
||||
"gray6": "#f4f5f8",
|
||||
"gray7": "#fbfbfb",
|
||||
"grayBlue": "#212327",
|
||||
"greenBase": "#299c46",
|
||||
"greenShade": "#23843b",
|
||||
"headingColor": "#e3e3e3",
|
||||
"inputBlack": "#09090b",
|
||||
"link": "#e3e3e3",
|
||||
"linkDisabled": "#e3e3e3",
|
||||
"linkExternal": "#33b5e5",
|
||||
"linkHover": "#ffffff",
|
||||
"online": "#299c46",
|
||||
"orange": "#eb7b18",
|
||||
"pageBg": "#161719",
|
||||
"purple": "#9933cc",
|
||||
"queryGreen": "#74e680",
|
||||
"queryKeyword": "#66d9ef",
|
||||
"queryOrange": "#eb7b18",
|
||||
"queryPurple": "#fe85fc",
|
||||
"queryRed": "#e02f44",
|
||||
"red": "#d44a3a",
|
||||
"redBase": "#e02f44",
|
||||
"redShade": "#c4162a",
|
||||
"text": "#d8d9da",
|
||||
"textEmphasis": "#ececec",
|
||||
"textFaint": "#222426",
|
||||
"textStrong": "#ffffff",
|
||||
"textWeak": "#8e8e8e",
|
||||
"variable": "#32d1df",
|
||||
"warn": "#f79520",
|
||||
"white": "#ffffff",
|
||||
"yellow": "#ecbb13",
|
||||
},
|
||||
"name": "Grafana Dark",
|
||||
"panelPadding": Object {
|
||||
"horizontal": 10,
|
||||
"vertical": 5,
|
||||
},
|
||||
"spacing": Object {
|
||||
"d": "14px",
|
||||
"gutter": "30px",
|
||||
"lg": "24px",
|
||||
"md": "16px",
|
||||
"sm": "8px",
|
||||
"xl": "32px",
|
||||
"xs": "4px",
|
||||
"xxs": "2px",
|
||||
},
|
||||
"type": "dark",
|
||||
"typography": Object {
|
||||
"fontFamily": Object {
|
||||
"monospace": "Menlo, Monaco, Consolas, 'Courier New', monospace",
|
||||
"sansSerif": "'Roboto', Helvetica, Arial, sans-serif",
|
||||
},
|
||||
"heading": Object {
|
||||
"h1": "28px",
|
||||
"h2": "24px",
|
||||
"h3": "21px",
|
||||
"h4": "18px",
|
||||
"h5": "16px",
|
||||
"h6": "14px",
|
||||
},
|
||||
"lineHeight": Object {
|
||||
"lg": 1.5,
|
||||
"md": 1.3333333333333333,
|
||||
"sm": 1.1,
|
||||
"xs": 1,
|
||||
},
|
||||
"size": Object {
|
||||
"base": "13px",
|
||||
"lg": "18px",
|
||||
"md": "14px",
|
||||
"root": "14px",
|
||||
"sm": "12px",
|
||||
"xs": "10px",
|
||||
},
|
||||
"weight": Object {
|
||||
"light": 300,
|
||||
"regular": 400,
|
||||
"semibold": 500,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<PopperController
|
||||
content={
|
||||
<ColorPickerPopover
|
||||
color="#7EB26D"
|
||||
enableNamedColors={true}
|
||||
onChange={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"background": Object {
|
||||
"dropdown": "#1f1f20",
|
||||
"scrollbar": "#343436",
|
||||
"scrollbar2": "#343436",
|
||||
},
|
||||
"border": Object {
|
||||
"radius": Object {
|
||||
"lg": "5px",
|
||||
"md": "3px",
|
||||
"sm": "2px",
|
||||
},
|
||||
"width": Object {
|
||||
"sm": "1px",
|
||||
},
|
||||
},
|
||||
"breakpoints": Object {
|
||||
"lg": "992px",
|
||||
"md": "768px",
|
||||
"sm": "544px",
|
||||
"xl": "1200px",
|
||||
"xs": "0",
|
||||
},
|
||||
"colors": Object {
|
||||
"black": "#000000",
|
||||
"blue": "#33b5e5",
|
||||
"blueBase": "#3274d9",
|
||||
"blueFaint": "#041126",
|
||||
"blueLight": "#5794f2",
|
||||
"blueShade": "#1f60c4",
|
||||
"body": "#d8d9da",
|
||||
"bodyBg": "#161719",
|
||||
"brandDanger": "#e02f44",
|
||||
"brandPrimary": "#eb7b18",
|
||||
"brandSuccess": "#299c46",
|
||||
"brandWarning": "#eb7b18",
|
||||
"critical": "#e02f44",
|
||||
"dark1": "#141414",
|
||||
"dark10": "#424345",
|
||||
"dark2": "#161719",
|
||||
"dark3": "#1f1f20",
|
||||
"dark4": "#212124",
|
||||
"dark5": "#222426",
|
||||
"dark6": "#262628",
|
||||
"dark7": "#292a2d",
|
||||
"dark8": "#2f2f32",
|
||||
"dark9": "#343436",
|
||||
"gray1": "#555555",
|
||||
"gray2": "#8e8e8e",
|
||||
"gray3": "#b3b3b3",
|
||||
"gray4": "#d8d9da",
|
||||
"gray5": "#ececec",
|
||||
"gray6": "#f4f5f8",
|
||||
"gray7": "#fbfbfb",
|
||||
"grayBlue": "#212327",
|
||||
"greenBase": "#299c46",
|
||||
"greenShade": "#23843b",
|
||||
"headingColor": "#e3e3e3",
|
||||
"inputBlack": "#09090b",
|
||||
"link": "#e3e3e3",
|
||||
"linkDisabled": "#e3e3e3",
|
||||
"linkExternal": "#33b5e5",
|
||||
"linkHover": "#ffffff",
|
||||
"online": "#299c46",
|
||||
"orange": "#eb7b18",
|
||||
"pageBg": "#161719",
|
||||
"purple": "#9933cc",
|
||||
"queryGreen": "#74e680",
|
||||
"queryKeyword": "#66d9ef",
|
||||
"queryOrange": "#eb7b18",
|
||||
"queryPurple": "#fe85fc",
|
||||
"queryRed": "#e02f44",
|
||||
"red": "#d44a3a",
|
||||
"redBase": "#e02f44",
|
||||
"redShade": "#c4162a",
|
||||
"text": "#d8d9da",
|
||||
"textEmphasis": "#ececec",
|
||||
"textFaint": "#222426",
|
||||
"textStrong": "#ffffff",
|
||||
"textWeak": "#8e8e8e",
|
||||
"variable": "#32d1df",
|
||||
"warn": "#f79520",
|
||||
"white": "#ffffff",
|
||||
"yellow": "#ecbb13",
|
||||
},
|
||||
"name": "Grafana Dark",
|
||||
"panelPadding": Object {
|
||||
"horizontal": 10,
|
||||
"vertical": 5,
|
||||
},
|
||||
"spacing": Object {
|
||||
"d": "14px",
|
||||
"gutter": "30px",
|
||||
"lg": "24px",
|
||||
"md": "16px",
|
||||
"sm": "8px",
|
||||
"xl": "32px",
|
||||
"xs": "4px",
|
||||
"xxs": "2px",
|
||||
},
|
||||
"type": "dark",
|
||||
"typography": Object {
|
||||
"fontFamily": Object {
|
||||
"monospace": "Menlo, Monaco, Consolas, 'Courier New', monospace",
|
||||
"sansSerif": "'Roboto', Helvetica, Arial, sans-serif",
|
||||
},
|
||||
"heading": Object {
|
||||
"h1": "28px",
|
||||
"h2": "24px",
|
||||
"h3": "21px",
|
||||
"h4": "18px",
|
||||
"h5": "16px",
|
||||
"h6": "14px",
|
||||
},
|
||||
"lineHeight": Object {
|
||||
"lg": 1.5,
|
||||
"md": 1.3333333333333333,
|
||||
"sm": 1.1,
|
||||
"xs": 1,
|
||||
},
|
||||
"size": Object {
|
||||
"base": "13px",
|
||||
"lg": "18px",
|
||||
"md": "14px",
|
||||
"root": "14px",
|
||||
"sm": "12px",
|
||||
"xs": "10px",
|
||||
},
|
||||
"weight": Object {
|
||||
"light": 300,
|
||||
"regular": 400,
|
||||
"semibold": 500,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
hideAfter={300}
|
||||
>
|
||||
<ForwardRef(ColorPickerTrigger)
|
||||
color="#7EB26D"
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
>
|
||||
<div
|
||||
onClick={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"background": "inherit",
|
||||
"border": "none",
|
||||
"borderRadius": 10,
|
||||
"color": "inherit",
|
||||
"cursor": "pointer",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundImage": "url(data:image/png,base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)",
|
||||
"border": "none",
|
||||
"float": "left",
|
||||
"height": 15,
|
||||
"margin": 0,
|
||||
"position": "relative",
|
||||
"width": 15,
|
||||
"zIndex": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#7EB26D",
|
||||
"bottom": 0,
|
||||
"display": "block",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ForwardRef(ColorPickerTrigger)>
|
||||
</PopperController>
|
||||
</ColorPicker>
|
||||
</WithTheme(ColorPicker)>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="thresholds-row-input-inner-value"
|
||||
>
|
||||
<input
|
||||
readOnly={true}
|
||||
type="text"
|
||||
value="Base"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Component>
|
||||
</ThresholdsEditor>
|
||||
`;
|
||||
|
||||
@@ -25,6 +25,7 @@ export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
|
||||
export { Switch } from './Switch/Switch';
|
||||
export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
|
||||
export { UnitPicker } from './UnitPicker/UnitPicker';
|
||||
export { Input, InputStatus } from './Input/Input';
|
||||
|
||||
// Visualizations
|
||||
export { Gauge } from './Gauge/Gauge';
|
||||
|
||||
@@ -5,3 +5,4 @@ export * from './plugin';
|
||||
export * from './datasource';
|
||||
export * from './theme';
|
||||
export * from './threshold';
|
||||
export * from './input';
|
||||
|
||||
8
packages/grafana-ui/src/types/input.ts
Normal file
8
packages/grafana-ui/src/types/input.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface ValidationRule {
|
||||
rule: (valueToValidate: string) => boolean;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
export interface ValidationEvents {
|
||||
[eventName: string]: ValidationRule[];
|
||||
}
|
||||
@@ -91,6 +91,7 @@ export interface PluginMeta {
|
||||
includes: PluginInclude[];
|
||||
|
||||
// Datasource-specific
|
||||
builtIn?: boolean;
|
||||
metrics?: boolean;
|
||||
tables?: boolean;
|
||||
logs?: boolean;
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from './string';
|
||||
export * from './displayValue';
|
||||
export * from './deprecationWarning';
|
||||
export { getMappedValue } from './valueMappings';
|
||||
export * from './validate';
|
||||
|
||||
24
packages/grafana-ui/src/utils/validate.ts
Normal file
24
packages/grafana-ui/src/utils/validate.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ValidationRule, ValidationEvents } from '../types/input';
|
||||
|
||||
export enum EventsWithValidation {
|
||||
onBlur = 'onBlur',
|
||||
onFocus = 'onFocus',
|
||||
onChange = 'onChange',
|
||||
}
|
||||
|
||||
export const validate = (value: string, validationRules: ValidationRule[]) => {
|
||||
const errors = validationRules.reduce(
|
||||
(acc, currRule) => {
|
||||
if (!currRule.rule(value)) {
|
||||
return acc.concat(currRule.errorMessage);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as string[]
|
||||
);
|
||||
return errors.length > 0 ? errors : null;
|
||||
};
|
||||
|
||||
export const hasValidationEvent = (event: EventsWithValidation, validationEvents: ValidationEvents | undefined) => {
|
||||
return validationEvents && validationEvents[event];
|
||||
};
|
||||
Reference in New Issue
Block a user