UI: New widget type 'HTML Card'

This commit is contained in:
Igor Kulikov 2017-02-06 11:55:43 +02:00
parent c860753bc8
commit d18ca19b3b
17 changed files with 191 additions and 36 deletions

File diff suppressed because one or more lines are too long

View File

@ -46,6 +46,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
$window.TbAnalogueRadialGauge = TbAnalogueRadialGauge; $window.TbAnalogueRadialGauge = TbAnalogueRadialGauge;
$window.TbDigitalGauge = TbDigitalGauge; $window.TbDigitalGauge = TbDigitalGauge;
$window.TbMapWidget = TbMapWidget; $window.TbMapWidget = TbMapWidget;
$window.cssjs = cssjs;
var cssParser = new cssjs(); var cssParser = new cssjs();
cssParser.testMode = false; cssParser.testMode = false;

View File

@ -141,6 +141,14 @@ export default angular.module('thingsboard.types', [])
bundleAlias: "gpio_widgets", bundleAlias: "gpio_widgets",
alias: "basic_gpio_control" alias: "basic_gpio_control"
} }
},
static: {
value: "static",
name: "widget.static",
template: {
bundleAlias: "cards",
alias: "html_card"
}
} }
}, },
systemBundleAlias: { systemBundleAlias: {

View File

@ -139,6 +139,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
vm.widgetBackgroundColor = widgetBackgroundColor; vm.widgetBackgroundColor = widgetBackgroundColor;
vm.widgetPadding = widgetPadding; vm.widgetPadding = widgetPadding;
vm.showWidgetTitle = showWidgetTitle; vm.showWidgetTitle = showWidgetTitle;
vm.dropWidgetShadow = dropWidgetShadow;
vm.hasTimewindow = hasTimewindow; vm.hasTimewindow = hasTimewindow;
vm.editWidget = editWidget; vm.editWidget = editWidget;
vm.exportWidget = exportWidget; vm.exportWidget = exportWidget;
@ -521,6 +522,14 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
} }
} }
function dropWidgetShadow(widget) {
if (angular.isDefined(widget.config.dropShadow)) {
return widget.config.dropShadow;
} else {
return true;
}
}
function hasTimewindow(widget) { function hasTimewindow(widget) {
return widget.type === types.widgetType.timeseries.value; return widget.type === types.widgetType.timeseries.value;
} }

View File

@ -28,8 +28,10 @@
<li gridster-item="widget" ng-repeat="widget in vm.widgets"> <li gridster-item="widget" ng-repeat="widget in vm.widgets">
<md-menu md-position-mode="target target" tb-mousepoint-menu> <md-menu md-position-mode="target target" tb-mousepoint-menu>
<div tb-expand-fullscreen <div tb-expand-fullscreen
expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget md-whiteframe-4dp" expand-button-id="expand-button" on-fullscreen-changed="vm.onWidgetFullscreenChanged(expanded, widget)" layout="column" class="tb-widget"
ng-class="{'tb-highlighted': vm.isHighlighted(widget), 'tb-not-highlighted': vm.isNotHighlighted(widget)}" ng-class="{'tb-highlighted': vm.isHighlighted(widget),
'tb-not-highlighted': vm.isNotHighlighted(widget),
'md-whiteframe-4dp': vm.dropWidgetShadow(widget)}"
tb-mousedown="vm.widgetMouseDown($event, widget)" tb-mousedown="vm.widgetMouseDown($event, widget)"
tb-mousemove="vm.widgetMouseMove($event, widget)" tb-mousemove="vm.widgetMouseMove($event, widget)"
tb-mouseup="vm.widgetMouseUp($event, widget)" tb-mouseup="vm.widgetMouseUp($event, widget)"

View File

@ -0,0 +1,41 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import ThingsboardAceEditor from './json-form-ace-editor.jsx';
import 'brace/mode/css';
import beautify from 'js-beautify';
const css_beautify = beautify.css;
class ThingsboardCss extends React.Component {
constructor(props) {
super(props);
this.onTidyCss = this.onTidyCss.bind(this);
}
onTidyCss(css) {
return css_beautify(css, {indent_size: 4});
}
render() {
return (
<ThingsboardAceEditor {...this.props} mode='css' onTidy={this.onTidyCss} {...this.state}></ThingsboardAceEditor>
);
}
}
export default ThingsboardCss;

View File

@ -0,0 +1,41 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import ThingsboardAceEditor from './json-form-ace-editor.jsx';
import 'brace/mode/html';
import beautify from 'js-beautify';
const html_beautify = beautify.html;
class ThingsboardHtml extends React.Component {
constructor(props) {
super(props);
this.onTidyHtml = this.onTidyHtml.bind(this);
}
onTidyHtml(html) {
return html_beautify(html, {indent_size: 4});
}
render() {
return (
<ThingsboardAceEditor {...this.props} mode='html' onTidy={this.onTidyHtml} {...this.state}></ThingsboardAceEditor>
);
}
}
export default ThingsboardHtml;

View File

@ -19,6 +19,8 @@ import { utils } from 'react-schema-form';
import ThingsboardArray from './json-form-array.jsx'; import ThingsboardArray from './json-form-array.jsx';
import ThingsboardJavaScript from './json-form-javascript.jsx'; import ThingsboardJavaScript from './json-form-javascript.jsx';
import ThingsboardJson from './json-form-json.jsx'; import ThingsboardJson from './json-form-json.jsx';
import ThingsboardHtml from './json-form-html.jsx';
import ThingsboardCss from './json-form-css.jsx';
import ThingsboardColor from './json-form-color.jsx' import ThingsboardColor from './json-form-color.jsx'
import ThingsboardRcSelect from './json-form-rc-select.jsx'; import ThingsboardRcSelect from './json-form-rc-select.jsx';
import ThingsboardNumber from './json-form-number.jsx'; import ThingsboardNumber from './json-form-number.jsx';
@ -52,6 +54,8 @@ class ThingsboardSchemaForm extends React.Component {
'array': ThingsboardArray, 'array': ThingsboardArray,
'javascript': ThingsboardJavaScript, 'javascript': ThingsboardJavaScript,
'json': ThingsboardJson, 'json': ThingsboardJson,
'html': ThingsboardHtml,
'css': ThingsboardCss,
'color': ThingsboardColor, 'color': ThingsboardColor,
'rc-select': ThingsboardRcSelect, 'rc-select': ThingsboardRcSelect,
'fieldset': ThingsboardFieldSet 'fieldset': ThingsboardFieldSet

View File

@ -74,11 +74,12 @@ function WidgetConfig($compile, $templateCache, $rootScope, types, utils) {
scope.selectedTab = 0; scope.selectedTab = 0;
scope.title = ngModelCtrl.$viewValue.title; scope.title = ngModelCtrl.$viewValue.title;
scope.showTitle = ngModelCtrl.$viewValue.showTitle; scope.showTitle = ngModelCtrl.$viewValue.showTitle;
scope.dropShadow = angular.isDefined(ngModelCtrl.$viewValue.dropShadow) ? ngModelCtrl.$viewValue.dropShadow : true;
scope.backgroundColor = ngModelCtrl.$viewValue.backgroundColor; scope.backgroundColor = ngModelCtrl.$viewValue.backgroundColor;
scope.color = ngModelCtrl.$viewValue.color; scope.color = ngModelCtrl.$viewValue.color;
scope.padding = ngModelCtrl.$viewValue.padding; scope.padding = ngModelCtrl.$viewValue.padding;
scope.timewindow = ngModelCtrl.$viewValue.timewindow; scope.timewindow = ngModelCtrl.$viewValue.timewindow;
if (scope.widgetType !== types.widgetType.rpc.value) { if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.static.value) {
if (scope.datasources) { if (scope.datasources) {
scope.datasources.splice(0, scope.datasources.length); scope.datasources.splice(0, scope.datasources.length);
} else { } else {
@ -89,7 +90,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, types, utils) {
scope.datasources.push({value: ngModelCtrl.$viewValue.datasources[i]}); scope.datasources.push({value: ngModelCtrl.$viewValue.datasources[i]});
} }
} }
} else { } else if (scope.widgetType === types.widgetType.rpc.value) {
if (ngModelCtrl.$viewValue.targetDeviceAliasIds && ngModelCtrl.$viewValue.targetDeviceAliasIds.length > 0) { if (ngModelCtrl.$viewValue.targetDeviceAliasIds && ngModelCtrl.$viewValue.targetDeviceAliasIds.length > 0) {
var aliasId = ngModelCtrl.$viewValue.targetDeviceAliasIds[0]; var aliasId = ngModelCtrl.$viewValue.targetDeviceAliasIds[0];
if (scope.deviceAliases[aliasId]) { if (scope.deviceAliases[aliasId]) {
@ -140,18 +141,19 @@ function WidgetConfig($compile, $templateCache, $rootScope, types, utils) {
if (scope.widgetType === types.widgetType.rpc.value) { if (scope.widgetType === types.widgetType.rpc.value) {
valid = value && value.targetDeviceAliasIds && value.targetDeviceAliasIds.length > 0; valid = value && value.targetDeviceAliasIds && value.targetDeviceAliasIds.length > 0;
ngModelCtrl.$setValidity('targetDeviceAliasIds', valid); ngModelCtrl.$setValidity('targetDeviceAliasIds', valid);
} else { } else if (scope.widgetType !== types.widgetType.static.value) {
valid = value && value.datasources && value.datasources.length > 0; valid = value && value.datasources && value.datasources.length > 0;
ngModelCtrl.$setValidity('datasources', valid); ngModelCtrl.$setValidity('datasources', valid);
} }
} }
}; };
scope.$watch('title + showTitle + backgroundColor + color + padding + intervalSec', function () { scope.$watch('title + showTitle + dropShadow + backgroundColor + color + padding + intervalSec', function () {
if (ngModelCtrl.$viewValue) { if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue; var value = ngModelCtrl.$viewValue;
value.title = scope.title; value.title = scope.title;
value.showTitle = scope.showTitle; value.showTitle = scope.showTitle;
value.dropShadow = scope.dropShadow;
value.backgroundColor = scope.backgroundColor; value.backgroundColor = scope.backgroundColor;
value.color = scope.color; value.color = scope.color;
value.padding = scope.padding; value.padding = scope.padding;
@ -177,7 +179,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, types, utils) {
}, true); }, true);
scope.$watch('datasources', function () { scope.$watch('datasources', function () {
if (ngModelCtrl.$viewValue && scope.widgetType !== types.widgetType.rpc.value) { if (ngModelCtrl.$viewValue && scope.widgetType !== types.widgetType.rpc.value
&& scope.widgetType !== types.widgetType.static.value) {
var value = ngModelCtrl.$viewValue; var value = ngModelCtrl.$viewValue;
if (value.datasources) { if (value.datasources) {
value.datasources.splice(0, value.datasources.length); value.datasources.splice(0, value.datasources.length);
@ -235,7 +238,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, types, utils) {
}; };
scope.updateDatasourcesAccordionState = function () { scope.updateDatasourcesAccordionState = function () {
if (scope.widgetType !== types.widgetType.rpc.value) { if (scope.widgetType !== types.widgetType.rpc.value &&
scope.widgetType !== types.widgetType.static.value) {
if (scope.datasourcesAccordion) { if (scope.datasourcesAccordion) {
scope.updateDatasourcesAccordionStatePending = false; scope.updateDatasourcesAccordionStatePending = false;
var expand = scope.datasources && scope.datasources.length < 4; var expand = scope.datasources && scope.datasources.length < 4;

View File

@ -31,6 +31,11 @@
ng-model="showTitle">{{ 'widget-config.display-title' | translate }} ng-model="showTitle">{{ 'widget-config.display-title' | translate }}
</md-checkbox> </md-checkbox>
</div> </div>
<div layout="row" layout-padding>
<md-checkbox flex aria-label="{{ 'widget-config.drop-shadow' | translate }}"
ng-model="dropShadow">{{ 'widget-config.drop-shadow' | translate }}
</md-checkbox>
</div>
<div flex <div flex
md-color-picker md-color-picker
ng-model="backgroundColor" ng-model="backgroundColor"
@ -64,7 +69,7 @@
<tb-timewindow as-button="true" flex ng-model="timewindow"></tb-timewindow> <tb-timewindow as-button="true" flex ng-model="timewindow"></tb-timewindow>
</div> </div>
<v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default" <v-accordion id="datasources-accordion" control="datasourcesAccordion" class="vAccordion--default"
ng-show="widgetType !== types.widgetType.rpc.value"> ng-show="widgetType !== types.widgetType.rpc.value && widgetType !== types.widgetType.static.value">
<v-pane id="datasources-pane" expanded="forceExpandDatasources"> <v-pane id="datasources-pane" expanded="forceExpandDatasources">
<v-pane-header> <v-pane-header>
{{ 'widget-config.datasources' | translate }} {{ 'widget-config.datasources' | translate }}

View File

@ -66,6 +66,10 @@ export default function AddWidgetController($scope, widgetService, deviceService
link = 'widgetsConfigRpc'; link = 'widgetsConfigRpc';
break; break;
} }
case types.widgetType.static.value: {
link = 'widgetsConfigStatic';
break;
}
} }
} }
return link; return link;

View File

@ -41,6 +41,7 @@ export default function DashboardController(types, widgetService, userService,
vm.latestWidgetTypes = []; vm.latestWidgetTypes = [];
vm.timeseriesWidgetTypes = []; vm.timeseriesWidgetTypes = [];
vm.rpcWidgetTypes = []; vm.rpcWidgetTypes = [];
vm.staticWidgetTypes = [];
vm.widgetEditMode = $state.$current.data.widgetEditMode; vm.widgetEditMode = $state.$current.data.widgetEditMode;
vm.widgets = []; vm.widgets = [];
@ -82,6 +83,7 @@ export default function DashboardController(types, widgetService, userService,
vm.latestWidgetTypes = []; vm.latestWidgetTypes = [];
vm.timeseriesWidgetTypes = []; vm.timeseriesWidgetTypes = [];
vm.rpcWidgetTypes = []; vm.rpcWidgetTypes = [];
vm.staticWidgetTypes = [];
if (vm.widgetsBundle) { if (vm.widgetsBundle) {
var bundleAlias = vm.widgetsBundle.alias; var bundleAlias = vm.widgetsBundle.alias;
var isSystem = vm.widgetsBundle.tenantId.id === types.id.nullUid; var isSystem = vm.widgetsBundle.tenantId.id === types.id.nullUid;
@ -127,6 +129,8 @@ export default function DashboardController(types, widgetService, userService,
vm.latestWidgetTypes.push(widget); vm.latestWidgetTypes.push(widget);
} else if (widgetTypeInfo.type === types.widgetType.rpc.value) { } else if (widgetTypeInfo.type === types.widgetType.rpc.value) {
vm.rpcWidgetTypes.push(widget); vm.rpcWidgetTypes.push(widget);
} else if (widgetTypeInfo.type === types.widgetType.static.value) {
vm.staticWidgetTypes.push(widget);
} }
top += sizeY; top += sizeY;
loadNextOrComplete(i); loadNextOrComplete(i);
@ -442,6 +446,10 @@ export default function DashboardController(types, widgetService, userService,
link = 'widgetsConfigRpc'; link = 'widgetsConfigRpc';
break; break;
} }
case types.widgetType.static.value: {
link = 'widgetsConfigStatic';
break;
}
} }
} }
return link; return link;
@ -490,6 +498,7 @@ export default function DashboardController(types, widgetService, userService,
vm.timeseriesWidgetTypes = []; vm.timeseriesWidgetTypes = [];
vm.latestWidgetTypes = []; vm.latestWidgetTypes = [];
vm.rpcWidgetTypes = []; vm.rpcWidgetTypes = [];
vm.staticWidgetTypes = [];
} }
function addWidgetFromType(event, widget) { function addWidgetFromType(event, widget) {

View File

@ -136,7 +136,7 @@
</header-pane> </header-pane>
<div> <div>
<md-tabs ng-if="vm.timeseriesWidgetTypes.length > 0 || vm.latestWidgetTypes.length > 0 || <md-tabs ng-if="vm.timeseriesWidgetTypes.length > 0 || vm.latestWidgetTypes.length > 0 ||
vm.rpcWidgetTypes.length > 0" vm.rpcWidgetTypes.length > 0 || vm.staticWidgetTypes.length > 0"
flex flex
class="tb-absolute-fill" md-border-bottom> class="tb-absolute-fill" md-border-bottom>
<md-tab ng-if="vm.timeseriesWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}"> <md-tab ng-if="vm.timeseriesWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}">
@ -169,9 +169,19 @@
on-widget-clicked="vm.addWidgetFromType(event, widget)"> on-widget-clicked="vm.addWidgetFromType(event, widget)">
</tb-dashboard> </tb-dashboard>
</md-tab> </md-tab>
<md-tab ng-if="vm.staticWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.static' | translate }}">
<tb-dashboard
widgets="vm.staticWidgetTypes"
is-edit="false"
is-mobile="true"
is-edit-action-enabled="false"
is-remove-action-enabled="false"
on-widget-clicked="vm.addWidgetFromType(event, widget)">
</tb-dashboard>
</md-tab>
</md-tabs> </md-tabs>
<span translate ng-if="vm.timeseriesWidgetTypes.length === 0 && vm.latestWidgetTypes.length === 0 && <span translate ng-if="vm.timeseriesWidgetTypes.length === 0 && vm.latestWidgetTypes.length === 0 &&
vm.rpcWidgetTypes.length === 0 && vm.widgetsBundle" vm.rpcWidgetTypes.length === 0 && vm.staticWidgetTypes.length === 0 && vm.widgetsBundle"
layout-align="center center" layout-align="center center"
style="text-transform: uppercase; display: flex;" style="text-transform: uppercase; display: flex;"
class="md-headline tb-absolute-fill">widgets-bundle.empty</span> class="md-headline tb-absolute-fill">widgets-bundle.empty</span>

View File

@ -305,31 +305,33 @@ export default function AttributeTableDirective($compile, $templateCache, $rootS
for (var i = 0; i < widgetTypes.length; i++) { for (var i = 0; i < widgetTypes.length; i++) {
var widgetType = widgetTypes[i]; var widgetType = widgetTypes[i];
var widgetInfo = widgetService.toWidgetInfo(widgetType); var widgetInfo = widgetService.toWidgetInfo(widgetType);
var sizeX = widgetInfo.sizeX*2; if (widgetInfo.type !== types.widgetType.static.value) {
var sizeY = widgetInfo.sizeY*2; var sizeX = widgetInfo.sizeX * 2;
var col = Math.floor(Math.max(0, (20 - sizeX)/2)); var sizeY = widgetInfo.sizeY * 2;
var widget = { var col = Math.floor(Math.max(0, (20 - sizeX) / 2));
isSystemType: isSystem, var widget = {
bundleAlias: bundleAlias, isSystemType: isSystem,
typeAlias: widgetInfo.alias, bundleAlias: bundleAlias,
type: widgetInfo.type, typeAlias: widgetInfo.alias,
title: widgetInfo.widgetName, type: widgetInfo.type,
sizeX: sizeX, title: widgetInfo.widgetName,
sizeY: sizeY, sizeX: sizeX,
row: 0, sizeY: sizeY,
col: col, row: 0,
config: angular.fromJson(widgetInfo.defaultConfig) col: col,
}; config: angular.fromJson(widgetInfo.defaultConfig)
};
widget.config.title = widgetInfo.widgetName; widget.config.title = widgetInfo.widgetName;
widget.config.datasources = [datasource]; widget.config.datasources = [datasource];
var length; var length;
if (scope.attributeScope === types.latestTelemetry && widgetInfo.type !== types.widgetType.rpc.value) { if (scope.attributeScope === types.latestTelemetry && widgetInfo.type !== types.widgetType.rpc.value) {
length = scope.widgetsListCache.push([widget]); length = scope.widgetsListCache.push([widget]);
scope.widgetsList.push(length === 1 ? [widget] : []); scope.widgetsList.push(length === 1 ? [widget] : []);
} else if (widgetInfo.type === types.widgetType.latest.value) { } else if (widgetInfo.type === types.widgetType.latest.value) {
length = scope.widgetsListCache.push([widget]); length = scope.widgetsListCache.push([widget]);
scope.widgetsList.push(length === 1 ? [widget] : []); scope.widgetsList.push(length === 1 ? [widget] : []);
}
} }
} }
scope.widgetsLoaded = true; scope.widgetsLoaded = true;

View File

@ -45,7 +45,7 @@ var pluginActionsClazzHelpLinkMap = {
'org.thingsboard.server.extensions.rest.action.RestApiCallPluginAction': 'pluginActionRestApiCall' 'org.thingsboard.server.extensions.rest.action.RestApiCallPluginAction': 'pluginActionRestApiCall'
}; };
var helpBaseUrl = "http://thingsboard.io"; var helpBaseUrl = "https://thingsboard.io";
export default angular.module('thingsboard.help', []) export default angular.module('thingsboard.help', [])
.constant('helpLinks', .constant('helpLinks',
@ -86,6 +86,7 @@ export default angular.module('thingsboard.help', [])
widgetsConfigTimeseries: helpBaseUrl + "/docs/user-guide/ui/dashboards#timeseries", widgetsConfigTimeseries: helpBaseUrl + "/docs/user-guide/ui/dashboards#timeseries",
widgetsConfigLatest: helpBaseUrl + "/docs/user-guide/ui/dashboards#latest", widgetsConfigLatest: helpBaseUrl + "/docs/user-guide/ui/dashboards#latest",
widgetsConfigRpc: helpBaseUrl + "/docs/user-guide/ui/dashboards#rpc", widgetsConfigRpc: helpBaseUrl + "/docs/user-guide/ui/dashboards#rpc",
widgetsConfigStatic: helpBaseUrl + "/docs/user-guide/ui/dashboards#static",
}, },
getPluginLink: function(plugin) { getPluginLink: function(plugin) {
var link = 'plugins'; var link = 'plugins';

View File

@ -53,6 +53,13 @@
</md-icon> </md-icon>
<span translate>{{vm.types.widgetType.rpc.name}}</span> <span translate>{{vm.types.widgetType.rpc.name}}</span>
</md-button> </md-button>
<md-button class="tb-card-button md-raised md-primary" layout="column"
ng-click="vm.typeSelected(vm.types.widgetType.static.value)">
<md-icon class="material-icons tb-md-96"
aria-label="{{ vm.types.widgetType.static.name | translate }}">font_download
</md-icon>
<span translate>{{vm.types.widgetType.static.name}}</span>
</md-button>
</div> </div>
</fieldset> </fieldset>
</div> </div>

View File

@ -603,6 +603,7 @@
"timeseries": "Time series", "timeseries": "Time series",
"latest-values": "Latest values", "latest-values": "Latest values",
"rpc": "Control widget", "rpc": "Control widget",
"static": "Static widget",
"select-widget-type": "Select widget type", "select-widget-type": "Select widget type",
"missing-widget-title-error": "Widget title must be specified!", "missing-widget-title-error": "Widget title must be specified!",
"widget-saved": "Widget saved", "widget-saved": "Widget saved",
@ -663,6 +664,7 @@
"title": "Title", "title": "Title",
"general-settings": "General settings", "general-settings": "General settings",
"display-title": "Display title", "display-title": "Display title",
"drop-shadow": "Drop shadow",
"background-color": "Background color", "background-color": "Background color",
"text-color": "Text color", "text-color": "Text color",
"padding": "Padding", "padding": "Padding",