Feat: Progress Elements (#23176) (#23618)

* Adds progress function and elements

Added progress elements

Added progress view

Added unit tests for progress common function

Fixed prop type in toggle arg

Renamed vert -> vertical and horiz -> horizontal

Adjusted progress element dimensions

Removed check for null context in progress function

Refactored progress shapes

Added unicorn shape

Adds labelPosition arg

Added tests for labelPosition

* Added percentage column to demodata

* Updated elements to use percent_uptime in demodata

* Updated demodata percent values

* Refactored progress to use SVGs instead of shape defs

* Added barWeight arg to progress function

* Removed labelPosition arg. Set static label position for each progress shape

* Added label to unicorn shape

* Fixed element images
This commit is contained in:
Catherine Liu 2018-09-28 12:54:14 -07:00 committed by GitHub
parent 739e3b2c7f
commit f77be52bf1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 2753 additions and 1028 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { openSans } from '../../../common/lib/fonts';
import header from './header.png';
export const horizontalProgressBar = () => ({
name: 'horizontalProgressBar',
displayName: 'Horizontal Progress Bar',
help: 'Displays progress as a portion of a horizontal bar',
width: 400,
height: 30,
image: header,
expression: `filters
| demodata
| math "mean(percent_uptime)"
| progress shape="horizontalBar" label={formatnumber 0%} font={font size=24 family="${
openSans.value
}" color="#000000" align=center}
| render`,
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { openSans } from '../../../common/lib/fonts';
import header from './header.png';
export const horizontalProgressPill = () => ({
name: 'horizontalProgressPill',
displayName: 'Horizontal Progress Pill',
help: 'Displays progress as a portion of a horizontal pill',
width: 400,
height: 30,
image: header,
expression: `filters
| demodata
| math "mean(percent_uptime)"
| progress shape="horizontalPill" label={formatnumber 0%} font={font size=24 family="${
openSans.value
}" color="#000000" align=center}
| render`,
});

View file

@ -11,11 +11,16 @@ import { donut } from './donut';
import { dropdownFilter } from './dropdown_filter';
import { image } from './image';
import { horizontalBarChart } from './horiz_bar_chart';
import { horizontalProgressBar } from './horizontal_progress_bar';
import { horizontalProgressPill } from './horizontal_progress_pill';
import { lineChart } from './line_chart';
import { markdown } from './markdown';
import { metric } from './metric';
import { pie } from './pie';
import { plot } from './plot';
import { progressGauge } from './progress_gauge';
import { progressSemicircle } from './progress_semicircle';
import { progressWheel } from './progress_wheel';
import { repeatImage } from './repeatImage';
import { revealImage } from './revealImage';
import { shape } from './shape';
@ -23,6 +28,8 @@ import { table } from './table';
import { tiltedPie } from './tilted_pie';
import { timeFilter } from './time_filter';
import { verticalBarChart } from './vert_bar_chart';
import { verticalProgressBar } from './vertical_progress_bar';
import { verticalProgressPill } from './vertical_progress_pill';
export const elementSpecs = [
areaChart,
@ -32,11 +39,16 @@ export const elementSpecs = [
dropdownFilter,
image,
horizontalBarChart,
horizontalProgressBar,
horizontalProgressPill,
lineChart,
markdown,
metric,
pie,
plot,
progressGauge,
progressSemicircle,
progressWheel,
repeatImage,
revealImage,
shape,
@ -44,4 +56,6 @@ export const elementSpecs = [
tiltedPie,
timeFilter,
verticalBarChart,
verticalProgressBar,
verticalProgressPill,
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { openSans } from '../../../common/lib/fonts';
import header from './header.png';
export const progressGauge = () => ({
name: 'progressGauge',
displayName: 'Progress Gauge',
help: 'Displays progress as a portion of a gauge',
width: 200,
height: 200,
image: header,
expression: `filters
| demodata
| math "mean(percent_uptime)"
| progress shape="gauge" label={formatnumber 0%} font={font size=24 family="${
openSans.value
}" color="#000000" align=center}
| render`,
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { openSans } from '../../../common/lib/fonts';
import header from './header.png';
export const progressSemicircle = () => ({
name: 'progressSemicircle',
displayName: 'Progress Semicircle',
help: 'Displays progress as a portion of a semicircle',
width: 200,
height: 100,
image: header,
expression: `filters
| demodata
| math "mean(percent_uptime)"
| progress shape="semicircle" label={formatnumber 0%} font={font size=24 family="${
openSans.value
}" color="#000000" align=center}
| render`,
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { openSans } from '../../../common/lib/fonts';
import header from './header.png';
export const progressWheel = () => ({
name: 'progressWheel',
displayName: 'Progress Wheel',
help: 'Displays progress as a portion of a wheel',
width: 200,
height: 200,
image: header,
expression: `filters
| demodata
| math "mean(percent_uptime)"
| progress shape="wheel" label={formatnumber 0%} font={font size=24 family="${
openSans.value
}" color="#000000" align=center}
| render`,
});

View file

@ -13,7 +13,7 @@ export const revealImage = () => ({
image: header,
expression: `filters
| demodata
| math "sum(min(cost) / max(cost))"
| math "mean(percent_uptime)"
| revealImage origin=bottom image=null
| render`,
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { openSans } from '../../../common/lib/fonts';
import header from './header.png';
export const verticalProgressBar = () => ({
name: 'verticalProgressBar',
displayName: 'Vertical Progress Bar',
help: 'Displays progress as a portion of a vertical bar',
width: 80,
height: 400,
image: header,
expression: `filters
| demodata
| math "mean(percent_uptime)"
| progress shape="verticalBar" label={formatnumber 0%} font={font size=24 family="${
openSans.value
}" color="#000000" align=center}
| render`,
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { openSans } from '../../../common/lib/fonts';
import header from './header.png';
export const verticalProgressPill = () => ({
name: 'verticalProgressPill',
displayName: 'Vertical Progress Pill',
help: 'Displays progress as a portion of a vertical pill',
width: 80,
height: 400,
image: header,
expression: `filters
| demodata
| math "mean(percent_uptime)"
| progress shape="verticalPill" label={formatnumber 0%} font={font size=24 family="${
openSans.value
}" color="#000000" align=center}
| render`,
});

View file

@ -0,0 +1,173 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
import { progress } from '../progress';
import { functionWrapper } from '../../../../__tests__/helpers/function_wrapper';
import { fontStyle } from './fixtures/test_styles';
describe('progress', () => {
const fn = functionWrapper(progress);
const value = 0.33;
it('returns a render as progress', () => {
const result = fn(0.2);
expect(result)
.to.have.property('type', 'render')
.and.to.have.property('as', 'progress');
});
it('sets the progress to context', () => {
const result = fn(0.58);
expect(result.value).to.have.property('value', 0.58);
});
it(`throws when context is outside of the valid range`, () => {
expect(fn)
.withArgs(3)
.to.throwException(e => {
expect(e.message).to.be('Context must be between 0 and 1');
});
});
describe('args', () => {
describe('shape', () => {
it('sets the progress element shape', () => {
const result = fn(value, {
shape: 'wheel',
});
expect(result.value).to.have.property('shape', 'wheel');
});
it(`defaults to 'gauge'`, () => {
const result = fn(value);
expect(result.value).to.have.property('shape', 'gauge');
});
});
describe('max', () => {
it('sets the maximum value', () => {
const result = fn(value, {
max: 2,
});
expect(result.value).to.have.property('max', 2);
});
it('defaults to 1', () => {
const result = fn(value);
expect(result.value).to.have.property('max', 1);
});
it('throws if max <= 0', () => {
expect(fn)
.withArgs(value, { max: -0.5 })
.to.throwException(e => {
expect(e.message).to.be(`'max' must be greater than 0`);
});
});
});
describe('valueColor', () => {
it('sets the color of the progress bar', () => {
const result = fn(value, {
valueColor: '#000000',
});
expect(result.value).to.have.property('valueColor', '#000000');
});
it(`defaults to '#1785b0'`, () => {
const result = fn(value);
expect(result.value).to.have.property('valueColor', '#1785b0');
});
});
describe('barColor', () => {
it('sets the color of the background bar', () => {
const result = fn(value, {
barColor: '#FFFFFF',
});
expect(result.value).to.have.property('barColor', '#FFFFFF');
});
it(`defaults to '#f0f0f0'`, () => {
const result = fn(value);
expect(result.value).to.have.property('barColor', '#f0f0f0');
});
});
describe('valueWeight', () => {
it('sets the thickness of the bars', () => {
const result = fn(value, {
valuWeight: 100,
});
expect(result.value).to.have.property('valuWeight', 100);
});
it(`defaults to 20`, () => {
const result = fn(value);
expect(result.value).to.have.property('barWeight', 20);
});
});
describe('barWeight', () => {
it('sets the thickness of the bars', () => {
const result = fn(value, {
barWeight: 50,
});
expect(result.value).to.have.property('barWeight', 50);
});
it(`defaults to 20`, () => {
const result = fn(value);
expect(result.value).to.have.property('barWeight', 20);
});
});
describe('label', () => {
it('sets the label of the progress', () => {
const result = fn(value, { label: 'foo' });
expect(result.value).to.have.property('label', 'foo');
});
it('hides the label if false', () => {
const result = fn(value, {
label: false,
});
expect(result.value).to.have.property('label', '');
});
it('defaults to true which sets the context as the label', () => {
const result = fn(value);
expect(result.value).to.have.property('label', '0.33');
});
});
describe('font', () => {
it('sets the font style for the label', () => {
const result = fn(value, {
font: fontStyle,
});
expect(result.value).to.have.property('font');
expect(result.value.font).to.have.keys(Object.keys(fontStyle));
expect(result.value.font.spec).to.have.keys(Object.keys(fontStyle.spec));
});
it('sets fill to color', () => {
const result = fn(value, {
font: fontStyle,
});
expect(result.value.font.spec).to.have.property('fill', fontStyle.spec.color);
});
// TODO: write test when using an instance of the interpreter
// it("sets a default style for the label when not provided", () => {});
});
});
});

View file

@ -40,6 +40,7 @@ import { palette } from './palette';
import { pie } from './pie';
import { plot } from './plot';
import { ply } from './ply';
import { progress } from './progress';
import { render } from './render';
import { replace } from './replace';
import { rounddate } from './rounddate';
@ -95,6 +96,7 @@ export const functions = [
pie,
plot,
ply,
progress,
render,
repeatImage,
replace,

View file

@ -0,0 +1,89 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { get } from 'lodash';
import { openSans } from '../../../common/lib/fonts';
import { shapes } from '../../renderers/progress/shapes';
export const progress = () => ({
name: 'progress',
aliases: [],
type: 'render',
help: 'Configure a progress element',
context: {
types: ['number'],
},
args: {
shape: {
type: ['string'],
alias: ['_'],
help: `Select ${Object.keys(shapes)
.map((key, i, src) => (i === src.length - 1 ? `or ${shapes[key].name}` : shapes[key].name))
.join(', ')}`,
default: 'gauge',
},
max: {
type: ['number'],
help: 'Maximum value of the progress element',
default: 1,
},
valueColor: {
type: ['string'],
help: 'Color of the progress bar',
default: `#1785b0`,
},
barColor: {
type: ['string'],
help: 'Color of the background bar',
default: `#f0f0f0`,
},
valueWeight: {
type: ['number'],
help: 'Thickness of the progress bar',
default: 20,
},
barWeight: {
type: ['number'],
help: 'Thickness of the background bar',
default: 20,
},
label: {
type: ['boolean', 'string'],
help: `Set true/false to show/hide label or provide a string to display as the label`,
default: true,
},
font: {
types: ['style'],
help: 'Font settings for the label. Technically you can stick other styles in here too!',
default: `{font size=24 family="${openSans.value}" color="#000000" align=center}`,
},
},
fn: (value, args) => {
if (args.max <= 0) throw new Error(`'max' must be greater than 0`);
if (value > args.max || value < 0) throw new Error(`Context must be between 0 and ${args.max}`);
let label = '';
if (args.label) label = typeof args.label === 'string' ? args.label : `${value}`;
let font = {};
if (get(args, 'font.spec')) {
font = { ...args.font };
font.spec.fill = args.font.spec.color; // SVG <text> uses fill for font color
}
return {
type: 'render',
as: 'progress',
value: {
value,
...args,
label,
font,
},
};
},
});

View file

@ -26,6 +26,7 @@ export const demodata = () => ({
},
fn: (context, args) => {
const demoRows = getDemoRows(args.type);
let set = {};
if (args.type === 'ci') {
set = {
@ -38,6 +39,7 @@ export const demodata = () => ({
{ name: 'country', type: 'string' },
{ name: 'state', type: 'string' },
{ name: 'project', type: 'string' },
{ name: 'percent_uptime', type: 'number' },
],
rows: sortBy(demoRows, 'time'),
};

View file

@ -15,6 +15,7 @@ import { markdown } from './markdown';
import { metric } from './metric';
import { pie } from './pie';
import { plot } from './plot';
import { progress } from './progress';
import { shape } from './shape';
import { table } from './table';
import { timeFilter } from './time_filter';
@ -32,6 +33,7 @@ export const renderFunctions = [
metric,
pie,
plot,
progress,
shape,
table,
timeFilter,

View file

@ -0,0 +1,115 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { getId } from '../../../public/lib/get_id';
import { shapes } from './shapes';
export const progress = () => ({
name: 'progress',
displayName: 'Progress',
help: 'Reveal a percentage of an element',
reuseDomNode: true,
render(domNode, config, handlers) {
const { shape, value, max, valueColor, barColor, valueWeight, barWeight, label, font } = config;
const percent = value / max;
const shapeDef = shapes[shape];
const offset = Math.max(valueWeight, barWeight);
if (shapeDef) {
const parser = new DOMParser();
const [shapeSvg] = parser
.parseFromString(shapes[shape], 'image/svg+xml')
.getElementsByTagName('svg');
const initialViewBox = shapeSvg
.getAttribute('viewBox')
.split(' ')
.map(v => parseInt(v, 10));
let [minX, minY, width, height] = initialViewBox;
if (shape !== 'horizontalBar') {
minX -= offset / 2;
width += offset;
}
if (shape === 'semicircle') {
minY -= offset / 2;
height += offset / 2;
} else if (shape !== 'verticalBar') {
minY -= offset / 2;
height += offset;
}
shapeSvg.setAttribute('className', 'canvasProgress');
const svgId = getId('svg');
shapeSvg.id = svgId;
const [bar] = shapeSvg.getElementsByTagName('path');
bar.setAttribute('className', 'canvasProgress__background');
bar.setAttribute('fill', 'none');
bar.setAttribute('stroke', barColor);
bar.setAttribute('stroke-width', `${barWeight}px`);
const value = bar.cloneNode(true);
value.setAttribute('className', 'canvasProgress__value');
value.setAttribute('stroke', valueColor);
value.setAttribute('stroke-width', `${valueWeight}px`);
const length = value.getTotalLength();
const to = length * (1 - percent);
value.setAttribute('stroke-dasharray', length);
value.setAttribute('stroke-dashoffset', Math.max(0, to));
shapeSvg.appendChild(value);
const [text] = shapeSvg.getElementsByTagName('text');
if (label && text) {
text.textContent = label;
text.setAttribute('className', 'canvasProgress__label');
if (shape === 'horizontalPill')
text.setAttribute('x', parseInt(text.getAttribute('x'), 10) + offset / 2);
if (shape === 'verticalPill')
text.setAttribute('y', parseInt(text.getAttribute('y'), 10) - offset / 2);
Object.assign(text.style, font.spec);
shapeSvg.appendChild(text);
domNode.appendChild(shapeSvg);
const { width: labelWidth, height: labelHeight } = text.getBBox();
if (shape === 'horizontalBar' || shape === 'horizontalPill') {
text.setAttribute('x', parseInt(text.getAttribute('x'), 10));
width += labelWidth;
}
if (shape === 'verticalBar' || shape === 'verticalPill') {
if (labelWidth > width) {
minX = -labelWidth / 2;
width = labelWidth;
}
minY -= labelHeight;
height += labelHeight;
}
}
shapeSvg.setAttribute('viewBox', [minX, minY, width, height].join(' '));
shapeSvg.setAttribute('width', domNode.offsetWidth);
shapeSvg.setAttribute('height', domNode.offsetHeight);
if (domNode.firstChild) domNode.removeChild(domNode.firstChild);
domNode.appendChild(shapeSvg);
handlers.onResize(() => {
shapeSvg.setAttribute('width', domNode.offsetWidth);
shapeSvg.setAttribute('height', domNode.offsetHeight);
});
}
handlers.done();
},
});

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120">
<path d="M 15 100 A 60 60 0 1 1 105 100" />
<text x="60" y="60" text-anchor='middle' dominant-baseline='central' />
</svg>

After

Width:  |  Height:  |  Size: 190 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 208 1">
<path d="M 0 1 L 200 1" />
<text x="208" y="0" text-anchor='start' dominant-baseline='central' />
</svg>

After

Width:  |  Height:  |  Size: 170 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 208 1">
<path d="M 0 1 L 200 1" stroke-linecap="round" />
<text x="208" y="0" text-anchor='start' dominant-baseline='central' />
</svg>

After

Width:  |  Height:  |  Size: 193 B

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import gauge from '!!raw-loader!./gauge.svg';
import horizontalBar from '!!raw-loader!./horizontal_bar.svg';
import horizontalPill from '!!raw-loader!./horizontal_pill.svg';
import semicircle from '!!raw-loader!./semicircle.svg';
import unicorn from '!!raw-loader!./unicorn.svg';
import verticalBar from '!!raw-loader!./vertical_bar.svg';
import verticalPill from '!!raw-loader!./vertical_pill.svg';
import wheel from '!!raw-loader!./wheel.svg';
export const shapes = {
gauge,
horizontalBar,
horizontalPill,
semicircle,
unicorn,
verticalBar,
verticalPill,
wheel,
};

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 60">
<path d="M 0 60 A 60 60 0 1 1 120 60" />
<text x="60" y="60" text-anchor='middle' dominant-baseline='text-after-edge' />
</svg>

After

Width:  |  Height:  |  Size: 194 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<path d="M 123 189 C 93 141 129 126 102 96 L 78 102 L 48 117 L 42 129 Q 30 132 21 126 L 18 114 L 27 90 L 42 72 L 48 57 L 3 6 L 57 42 L 63 33 L 60 15 L 69 27 L 69 15 L 84 27 Q 162 36 195 108 Q 174 159 123 189 Z" />
<text x="0" y="200" text-anchor='start' dominant-baseline='text-after-edge' />
</svg>

After

Width:  |  Height:  |  Size: 368 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -8 1 208">
<path d="M 1 200 L 1 0" />
<text x="0" y="-8" text-anchor='middle' dominant-baseline='text-after-edge' />
</svg>

After

Width:  |  Height:  |  Size: 180 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -8 1 208">
<path d="M 1 200 L 1 0" stroke-linecap="round" />
<text x="0" y="-8" text-anchor='middle' dominant-baseline='text-after-edge' />
</svg>

After

Width:  |  Height:  |  Size: 203 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120">
<path d="M 60 0 A 60 60 0 1 1 60 120 A 60 60 0 1 1 60 0 Z" />
<text x="60" y="60" text-anchor='middle' dominant-baseline='central' />
</svg>

After

Width:  |  Height:  |  Size: 208 B

View file

@ -7,14 +7,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { shapes } from '../../renderers/shape/shapes';
import { templateFromReactComponent } from '../../../public/lib/template_from_react_component';
import { ShapePickerMini } from '../../../public/components/shape_picker_mini/';
const ShapeArgInput = ({ onValueChange, argValue }) => (
const ShapeArgInput = ({ onValueChange, argValue, typeInstance }) => (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<ShapePickerMini value={argValue} onChange={onValueChange} shapes={shapes} />
<ShapePickerMini
value={argValue}
onChange={onValueChange}
shapes={typeInstance.options.shapes}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
@ -22,6 +25,9 @@ const ShapeArgInput = ({ onValueChange, argValue }) => (
ShapeArgInput.propTypes = {
argValue: PropTypes.any.isRequired,
onValueChange: PropTypes.func.isRequired,
typeInstance: PropTypes.shape({
options: PropTypes.shape({ shapes: PropTypes.object.isRequired }).isRequired,
}).isRequired,
};
export const shape = () => ({

View file

@ -9,16 +9,17 @@ import PropTypes from 'prop-types';
import { EuiSwitch } from '@elastic/eui';
import { templateFromReactComponent } from '../../../public/lib/template_from_react_component';
const ToggleArgInput = ({ onValueChange, argValue, argId }) => {
const ToggleArgInput = ({ onValueChange, argValue, argId, renderError }) => {
const handleChange = () => onValueChange(!argValue);
if (typeof argValue !== 'boolean') renderError();
return <EuiSwitch id={argId} checked={argValue} onChange={handleChange} />;
};
ToggleArgInput.propTypes = {
onValueChange: PropTypes.func.isRequired,
argValue: PropTypes.bool.isRequired,
argValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.string, PropTypes.object]).isRequired,
argId: PropTypes.string.isRequired,
renderError: PropTypes.func.isRequired,
};
export const toggle = () => ({

View file

@ -5,12 +5,13 @@
*/
import { dropdownControl } from './dropdownControl';
import { getCell } from './getCell';
import { image } from './image';
import { markdown } from './markdown';
import { metric } from './metric';
import { pie } from './pie';
import { plot } from './plot';
import { getCell } from './getCell';
import { progress } from './progress';
import { repeatImage } from './repeatImage';
import { revealImage } from './revealImage';
import { render } from './render';
@ -20,12 +21,13 @@ import { timefilterControl } from './timefilterControl';
export const viewSpecs = [
dropdownControl,
getCell,
image,
markdown,
metric,
pie,
plot,
getCell,
progress,
repeatImage,
revealImage,
render,

View file

@ -0,0 +1,77 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { openSans } from '../../../common/lib/fonts';
import { shapes } from '../../renderers/progress/shapes';
export const progress = () => ({
name: 'progress',
displayName: 'Progress',
modelArgs: [['_', { label: 'Value' }]],
requiresContext: false,
args: [
{
name: 'shape',
argType: 'select',
options: {
choices: Object.keys(shapes).map(key => ({
value: key,
//turns camel into title case
name: key[0].toUpperCase() + key.slice(1).replace(/([A-Z])/g, ' $1'),
})),
},
},
{
name: 'max',
displayName: 'Maximum value',
help: 'Maximum value of the progress element',
argType: 'number',
default: '1',
},
{
name: 'valueColor',
displayName: 'Progress Color',
help: 'Color of the progress bar',
argType: 'color',
default: `#1785b0`,
},
{
name: 'valueWeight',
displayName: 'Progress Weight',
help: 'Thickness of the progress bar',
argType: 'number',
default: '20',
},
{
name: 'barColor',
displayName: 'Background Color',
help: 'Color of the background bar',
argType: 'color',
default: `#f0f0f0`,
},
{
name: 'barWeight',
displayName: 'Background Weight',
help: 'Thickness of the background bar',
argType: 'number',
default: '20',
},
{
name: 'label',
displayName: 'Label',
help: `Set true/false to show/hide label or provide a string to display as the label`,
argType: 'toggle',
default: 'true',
},
{
name: 'font',
displayName: 'Label Settings',
help: 'Font settings for the label. Technically you can stick other styles in here too!',
argType: 'font',
default: `{font size=24 family="${openSans.value}" color="#000000" align=center}`,
},
],
});

View file

@ -16,12 +16,8 @@ export const shape = () => ({
name: '_',
displayName: 'Select a Shape',
argType: 'shape',
help: 'A basic shape',
options: {
choices: Object.keys(shapes).map(shape => ({
value: shape,
name: shape,
})),
shapes,
},
},
{

View file

@ -7,10 +7,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { EuiFlexGrid, EuiFlexItem, EuiLink } from '@elastic/eui';
import { shapes } from '../../../canvas_plugin_src/renderers/shape/shapes';
import { ShapePreview } from '../shape_preview';
export const ShapePicker = ({ onChange }) => {
export const ShapePicker = ({ shapes, onChange }) => {
return (
<EuiFlexGrid gutterSize="s" columns={4}>
{Object.keys(shapes)
@ -18,7 +17,7 @@ export const ShapePicker = ({ onChange }) => {
.map(shapeKey => (
<EuiFlexItem key={shapeKey}>
<EuiLink onClick={() => onChange(shapeKey)}>
<ShapePreview value={shapeKey} />
<ShapePreview shape={shapes[shapeKey]} />
</EuiLink>
</EuiFlexItem>
))}
@ -27,6 +26,6 @@ export const ShapePicker = ({ onChange }) => {
};
ShapePicker.propTypes = {
value: PropTypes.string,
shapes: PropTypes.object.isRequired,
onChange: PropTypes.func,
};

View file

@ -11,10 +11,10 @@ import { Popover } from '../popover';
import { ShapePicker } from '../shape_picker/';
import { ShapePreview } from '../shape_preview';
export const ShapePickerMini = ({ onChange, value, anchorPosition }) => {
export const ShapePickerMini = ({ shapes, onChange, value, anchorPosition }) => {
const button = handleClick => (
<EuiLink style={{ fontSize: 0 }} onClick={handleClick}>
<ShapePreview value={value} />
<ShapePreview shape={shapes[value]} />
</EuiLink>
);
@ -24,12 +24,13 @@ export const ShapePickerMini = ({ onChange, value, anchorPosition }) => {
button={button}
anchorPosition={anchorPosition}
>
{() => <ShapePicker onChange={onChange} value={value} />}
{() => <ShapePicker onChange={onChange} shapes={shapes} />}
</Popover>
);
};
ShapePickerMini.propTypes = {
shapes: PropTypes.object.isRequired,
value: PropTypes.string,
onChange: PropTypes.func,
anchorPosition: PropTypes.string,

View file

@ -6,13 +6,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import { shapes } from '../../../canvas_plugin_src/renderers/shape/shapes';
export const ShapePreview = ({ value }) => {
// eslint-disable-next-line react/no-danger
return <div className="canvasShapePreview" dangerouslySetInnerHTML={{ __html: shapes[value] }} />;
export const ShapePreview = ({ shape }) => {
const weight = 5;
const parser = new DOMParser();
const [shapeSvg] = parser.parseFromString(shape, 'image/svg+xml').getElementsByTagName('svg');
shapeSvg.setAttribute('fill', 'none');
shapeSvg.setAttribute('stroke', 'black');
const initialViewBox = shapeSvg
.getAttribute('viewBox')
.split(' ')
.map(v => parseInt(v, 10));
let [minX, minY, width, height] = initialViewBox;
minX -= weight / 2;
minY -= weight / 2;
width += weight;
height += weight;
shapeSvg.setAttribute('viewBox', [minX, minY, width, height].join(' '));
return (
// eslint-disable-next-line react/no-danger
<div className="canvasShapePreview" dangerouslySetInnerHTML={{ __html: shapeSvg.outerHTML }} />
);
};
ShapePreview.propTypes = {
value: PropTypes.string,
shape: PropTypes.string,
};

View file

@ -3,7 +3,6 @@
height: $euiSizeXXL;
svg {
fill: black;
width: 100%;
height: 100%;
}