Commit a85b3f38 by Tobias Kaupat

C3 gauge widget and widgetHelper

parent 78ae747b
Pipeline #4868353 passed with stages
in 4 minutes 23 seconds
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
(function () {
const TYPE_INFO = {
type: "c3-gauge",
name: "C3 Gauge",
version: "0.0.1",
author: "Lobaro",
kind: "widget",
description: "Renders a Gauge using the C3 library. The gauge always shows a property from the last datasource value.",
dependencies: [
"https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.16/d3.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.10/c3.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.10/c3.min.css"
],
settings: [
{
id: 'datasource',
name: 'Datasource',
type: 'datasource',
description: "The data source from which the last value is used as gauge value (you can specify a dataPath below)."
},
{
id: 'dataPath',
type: "string",
name: "Data Path",
description: "The path to get the data from the last data source value, e.g. nested.array[4] - do not use quoted between []",
defaultValue: ''
},
{
id: 'min',
type: "number",
name: "Min value",
description: "Set min value of the gauge.",
defaultValue: 0
},
{
id: 'max',
type: "number",
name: "Max value",
description: "Set max value of the gauge.",
defaultValue: 100
},
{
id: 'units',
type: "string",
name: "Units",
description: "Set units of the gauge.",
defaultValue: " %"
},
{
id: 'showLabel',
type: "boolean",
name: "Show Label",
description: "Show or hide label on gauge.",
defaultValue: true
},
{
id: 'colors',
type: "json",
name: "Colors (left to right)",
description: "Array of color values from left to right.",
defaultValue: '["#FF0000","#F97600","#F6C600","#60B044"]'
},
{
id: 'colorThreshold',
type: "json",
name: "Color Threshold (left to right)",
description: "Thresholds to change colors.",
defaultValue: "[10, 60, 90, 100]"
}
]
};
function safeParseJsonArray(string) {
try {
return JSON.parse(string);
}
catch (e) {
console.error("Was not able to parse JSON: " + string);
return []
}
}
class Widget extends React.Component {
componentDidMount() {
this._createChart(this.props);
}
componentWillReceiveProps(nextProps) {
if (nextProps.state.settings !== this.props.state.settings
|| nextProps.state.height !== this.props.state.height) {
this._createChart(nextProps);
}
}
getData() {
const props = this.props;
const settings = props.state.settings;
let data = props.getData(settings.datasource);
if (data.length > 0) {
data = data[data.length - 1]
}
return widgetHelper.propertyByString(data, settings['dataPath']) || 0;
}
_createChart(props) {
const config = props.state.settings;
this.chart = c3.generate({
bindto: '#chart-' + props.state.id,
size: {
height: props.state.availableHeightPx - 20
},
data: {
columns: [
['data', this.getData()]
],
type: 'gauge'
},
gauge: {
min: config['min'],
max: config['max'],
units: config['units'],
label: {
show: config['showLabel'],
format: function (value, ratio) {
return value;
}
},
expand: false
},
color: {
pattern: safeParseJsonArray(config['colors']),
threshold: {
values: safeParseJsonArray(config['colorThreshold'])
}
},
transition: {
duration: 0
}
})
}
_renderChart() {
if (!this.chart) {
return;
}
this.chart.load({
columns: [
['data', this.getData()]
]
});
}
render() {
this._renderChart();
return <div id={'chart-' + this.props.state.id}></div>
}
componentWillUnmount() {
console.log("Unmounted Chart Widget");
}
dispose() {
console.log("Disposed Chart Widget");
}
}
// TODO: Move to core, for simple reuse
const Prop = React.PropTypes
Widget.propTypes = {
getData: Prop.func.isRequired,
state: Prop.shape({
height: Prop.number.isRequired,
id: Prop.string.isRequired
}).isRequired
};
window.iotDashboardApi.registerWidgetPlugin(TYPE_INFO, Widget);
})();
......@@ -19,6 +19,7 @@ import './pluginApi/pluginLoader.test.ts'
import './pluginApi/uri.test.js'
import './serverRenderer.test.ts'
import './util/collection.test.js'
import './widgetApp/widgetHelper.test.ts'
import './widgets/widgetPlugins.test.ts'
import './widgets/widgets.test.ts'
/* endinject */
......
......@@ -17,8 +17,8 @@ const DashboardTopNavItem = (props) => {
<a href="javascript:void(0);" className="slds-context-bar__label-action" title="Dashboard">
<span className="slds-truncate">Board</span>
</a>
<div className="slds-context-bar__icon-action slds-p-left--none" tabindex="0">
<button className="slds-button slds-button--icon slds-context-bar__button" tabindex="-1">
<div className="slds-context-bar__icon-action slds-p-left--none" tabIndex="0">
<button className="slds-button slds-button--icon slds-context-bar__button" tabIndex="-1">
<svg aria-hidden="true" className="slds-button__icon">
<use xlinkHref="assets/icons/utility-sprite/svg/symbols.svg#chevrondown"></use>
</svg>
......
......@@ -18,8 +18,8 @@ const DatasourceTopNavItem = (props) => {
<a href="javascript:void(0);" className="slds-context-bar__label-action" title="Datasources">
<span className="slds-truncate">Datasources</span>
</a>
<div className="slds-context-bar__icon-action slds-p-left--none" tabindex="0">
<button className="slds-button slds-button--icon slds-context-bar__button" tabindex="-1">
<div className="slds-context-bar__icon-action slds-p-left--none" tabIndex="0">
<button className="slds-button slds-button--icon slds-context-bar__button" tabIndex="-1">
<svg aria-hidden="true" className="slds-button__icon">
<use xlinkHref="assets/icons/utility-sprite/svg/symbols.svg#chevrondown"></use>
</svg>
......
......@@ -16,8 +16,8 @@ const LayoutsTopNavItem = (props) => {
<a href="javascript:void(0);" className="slds-context-bar__label-action" title="Layouts">
<span className="slds-truncate">Layout</span>
</a>
<div className="slds-context-bar__icon-action slds-p-left--none" tabindex="0">
<button className="slds-button slds-button--icon slds-context-bar__button" tabindex="-1">
<div className="slds-context-bar__icon-action slds-p-left--none" tabIndex="0">
<button className="slds-button slds-button--icon slds-context-bar__button" tabIndex="-1">
<svg aria-hidden="true" className="slds-button__icon">
<use xlinkHref="assets/icons/utility-sprite/svg/symbols.svg#chevrondown"></use>
</svg>
......
......@@ -12,6 +12,7 @@ import './pluginApi/pluginLoader.test.ts'
import './pluginApi/uri.test.js'
import './serverRenderer.test.ts'
import './util/collection.test.js'
import './widgetApp/widgetHelper.test.ts'
import './widgets/widgetPlugins.test.ts'
import './widgets/widgets.test.ts'
/* endinject */
......
......@@ -41,7 +41,7 @@ export const DropdownItem = (props) => {
e.preventDefault();
props.onClick(e);
}}
tabindex="-1">
tabIndex="-1">
<span className="slds-truncate">{icon} {props.text}</span>{iconRight}
</a>
</li>
......
import "expose?$!expose?jQuery!jquery";
import "expose?React!react";
import "expose?_!lodash";
import helper from "./widgetHelper";
import "file?name=[name].[ext]!./widget.html";
import * as React from "react";
import {ITypeInfo, IWidgetProps} from "../pluginApi/pluginTypes";
......@@ -25,4 +26,6 @@ const pluginApi = {
// TO be robust during tests in node and server side rendering
if (window) {
(<any>window).iotDashboardApi = pluginApi;
(<any>window).widgetHelper = helper;
}
import {assert} from "chai";
import helper from "./widgetHelper";
describe('Widget Helper', function () {
describe('propertyByString', function () {
it("Get some valid properties", function () {
const obj = {
array: [0, 1, 2, 3, 4, 5],
string: "012345",
number: 12345,
nested: {
array: [0, 1, 2, 3, 4, 5],
string: "012345",
number: 12345,
}
};
assert.deepEqual([0, 1, 2, 3, 4, 5], helper.propertyByString(obj, ".array"));
assert.deepEqual("012345", helper.propertyByString(obj, "string"));
assert.deepEqual(12345, helper.propertyByString(obj, "[number]"));
assert.deepEqual([0, 1, 2, 3, 4, 5], helper.propertyByString(obj, "nested[array]"));
assert.deepEqual(0, helper.propertyByString(obj, "nested[array][0]"));
assert.deepEqual(1, helper.propertyByString(obj, "nested.array[1]"));
assert.deepEqual(2, helper.propertyByString(obj, "nested.array.2"));
});
});
});
export default class WidgetHelper {
static propertyByString(obj: any, path: string) {
if (!path) {
return obj;
}
path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
path = path.replace(/^\./, ''); // strip a leading dot
const tokens = path.split('.');
for (let i = 0, n = tokens.length; i < n; ++i) {
let tok = tokens[i];
if (obj != null && tok in obj) {
obj = obj[tok];
} else {
return;
}
}
return obj;
}
}
......@@ -20,8 +20,8 @@ const WidgetsNavItem = (props) => {
<a href="javascript:void(0);" className="slds-context-bar__label-action" title="Widgets">
<span className="slds-truncate">Add Widget</span>
</a>
<div className="slds-context-bar__icon-action slds-p-left--none" tabindex="0">
<button className="slds-button slds-button--icon slds-context-bar__button" tabindex="-1">
<div className="slds-context-bar__icon-action slds-p-left--none" tabIndex="0">
<button className="slds-button slds-button--icon slds-context-bar__button" tabIndex="-1">
<svg aria-hidden="true" className="slds-button__icon">
<use xlinkHref="assets/icons/utility-sprite/svg/symbols.svg#chevrondown"></use>
</svg>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment