Browse Source

Merge remote-tracking branch 'origin/master' into develop/3.3.1

pull/5132/head
Andrii Shvaika 5 years ago
parent
commit
99167d6c93
  1. 18
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 4
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  3. 14
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  4. 3
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  5. 3
      application/src/main/resources/thingsboard.yml
  6. 3
      application/src/test/resources/application-test.properties
  7. 2
      msa/tb/pom.xml
  8. 9
      ui-ngx/angular.json
  9. 1
      ui-ngx/package.json
  10. 18
      ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts
  11. 18
      ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html
  12. 130
      ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.scss
  13. 116
      ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts
  14. 2
      ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts
  15. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  16. 4
      ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx
  17. 33
      ui-ngx/src/app/shared/components/json-form/react/json-form-markdown.tsx
  18. 4
      ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx
  19. 99
      ui-ngx/src/app/shared/components/markdown.factory.ts
  20. 15
      ui-ngx/src/app/shared/shared.module.ts
  21. 39
      ui-ngx/yarn.lock

18
application/src/main/data/json/system/widget_bundles/cards.json

File diff suppressed because one or more lines are too long

4
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -400,9 +400,9 @@ public class ActorSystemContext {
@Getter @Getter
private String debugPerTenantLimitsConfiguration; private String debugPerTenantLimitsConfiguration;
@Value("${actors.rpc.sequence.enabled:false}") @Value("${actors.rpc.sequential:false}")
@Getter @Getter
private boolean rpcSequenceEnabled; private boolean rpcSequential;
@Value("${actors.rpc.max_retries:5}") @Value("${actors.rpc.max_retries:5}")
@Getter @Getter

14
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -122,7 +122,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
private final Map<UUID, SessionInfo> attributeSubscriptions; private final Map<UUID, SessionInfo> attributeSubscriptions;
private final Map<UUID, SessionInfo> rpcSubscriptions; private final Map<UUID, SessionInfo> rpcSubscriptions;
private final Map<Integer, ToDeviceRpcRequestMetadata> toDeviceRpcPendingMap; private final Map<Integer, ToDeviceRpcRequestMetadata> toDeviceRpcPendingMap;
private final boolean rpcSequenceEnabled; private final boolean rpcSequential;
private int rpcSeq = 0; private int rpcSeq = 0;
private String deviceName; private String deviceName;
@ -134,7 +134,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
super(systemContext); super(systemContext);
this.tenantId = tenantId; this.tenantId = tenantId;
this.deviceId = deviceId; this.deviceId = deviceId;
this.rpcSequenceEnabled = systemContext.isRpcSequenceEnabled(); this.rpcSequential = systemContext.isRpcSequential();
this.attributeSubscriptions = new HashMap<>(); this.attributeSubscriptions = new HashMap<>();
this.rpcSubscriptions = new HashMap<>(); this.rpcSubscriptions = new HashMap<>();
this.toDeviceRpcPendingMap = new LinkedHashMap<>(); this.toDeviceRpcPendingMap = new LinkedHashMap<>();
@ -233,7 +233,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
} }
private boolean isSendNewRpcAvailable() { private boolean isSendNewRpcAvailable() {
return !rpcSequenceEnabled || toDeviceRpcPendingMap.values().stream().filter(md -> !md.isDelivered()).findAny().isEmpty(); return !rpcSequential || toDeviceRpcPendingMap.values().stream().filter(md -> !md.isDelivered()).findAny().isEmpty();
} }
private Rpc createRpc(ToDeviceRpcRequest request, RpcStatus status) { private Rpc createRpc(ToDeviceRpcRequest request, RpcStatus status) {
@ -332,7 +332,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
} }
Set<Integer> sentOneWayIds = new HashSet<>(); Set<Integer> sentOneWayIds = new HashSet<>();
if (rpcSequenceEnabled) { if (rpcSequential) {
getFirstRpc().ifPresent(processPendingRpc(context, sessionId, nodeId, sentOneWayIds)); getFirstRpc().ifPresent(processPendingRpc(context, sessionId, nodeId, sentOneWayIds));
} else if (sessionType == SessionType.ASYNC) { } else if (sessionType == SessionType.ASYNC) {
toDeviceRpcPendingMap.entrySet().forEach(processPendingRpc(context, sessionId, nodeId, sentOneWayIds)); toDeviceRpcPendingMap.entrySet().forEach(processPendingRpc(context, sessionId, nodeId, sentOneWayIds));
@ -348,7 +348,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
} }
private void sendNextPendingRequest(TbActorCtx context) { private void sendNextPendingRequest(TbActorCtx context) {
if (rpcSequenceEnabled) { if (rpcSequential) {
rpcSubscriptions.forEach((id, s) -> sendPendingRequests(context, id, s.getNodeId())); rpcSubscriptions.forEach((id, s) -> sendPendingRequests(context, id, s.getNodeId()));
} }
} }
@ -357,7 +357,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
return entry -> { return entry -> {
ToDeviceRpcRequest request = entry.getValue().getMsg().getMsg(); ToDeviceRpcRequest request = entry.getValue().getMsg().getMsg();
ToDeviceRpcRequestBody body = request.getBody(); ToDeviceRpcRequestBody body = request.getBody();
if (request.isOneway() && !rpcSequenceEnabled) { if (request.isOneway() && !rpcSequential) {
sentOneWayIds.add(entry.getKey()); sentOneWayIds.add(entry.getKey());
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(request.getId(), null, null)); systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(request.getId(), null, null));
} }
@ -599,7 +599,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
if (status.equals(RpcStatus.DELIVERED)) { if (status.equals(RpcStatus.DELIVERED)) {
if (md.getMsg().getMsg().isOneway()) { if (md.getMsg().getMsg().isOneway()) {
toDeviceRpcPendingMap.remove(responseMsg.getRequestId()); toDeviceRpcPendingMap.remove(responseMsg.getRequestId());
if (rpcSequenceEnabled) { if (rpcSequential) {
systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(rpcId, null, null)); systemContext.getTbCoreDeviceRpcService().processRpcResponseFromDeviceActor(new FromDeviceRpcResponse(rpcId, null, null));
} }
} else { } else {

3
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -200,7 +200,8 @@ public class ThingsboardInstallService {
dataUpdateService.updateData("3.2.2"); dataUpdateService.updateData("3.2.2");
systemDataLoaderService.createOAuth2Templates(); systemDataLoaderService.createOAuth2Templates();
case "3.3.0":
log.info("Upgrading ThingsBoard from version 3.3.0 to 3.3.1 ...");
log.info("Updating system data..."); log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets(); systemDataLoaderService.updateSystemWidgets();
break; break;

3
application/src/main/resources/thingsboard.yml

@ -328,8 +328,7 @@ actors:
duration: "${ACTORS_RULE_TRANSACTION_DURATION:60000}" duration: "${ACTORS_RULE_TRANSACTION_DURATION:60000}"
rpc: rpc:
max_retries: "${ACTORS_RPC_MAX_RETRIES:5}" max_retries: "${ACTORS_RPC_MAX_RETRIES:5}"
sequence: sequential: "${ACTORS_RPC_SEQUENTIAL:false}"
enabled: "${ACTORS_RPC_SEQUENCE_ENABLED:false}"
statistics: statistics:
# Enable/disable actor statistics # Enable/disable actor statistics
enabled: "${ACTORS_STATISTICS_ENABLED:true}" enabled: "${ACTORS_STATISTICS_ENABLED:true}"

3
application/src/test/resources/application-test.properties

@ -6,4 +6,5 @@ edges.storage.sleep_between_batches=500
transport.lwm2m.server.security.key_alias=server transport.lwm2m.server.security.key_alias=server
transport.lwm2m.server.security.key_password=server transport.lwm2m.server.security.key_password=server
transport.lwm2m.bootstrap.security.key_alias=server transport.lwm2m.bootstrap.security.key_alias=server
transport.lwm2m.bootstrap.security.key_password=server transport.lwm2m.bootstrap.security.key_password=server
actors.rpc.sequential=true

2
msa/tb/pom.xml

@ -39,7 +39,7 @@
<tb-postgres.docker.name>tb-postgres</tb-postgres.docker.name> <tb-postgres.docker.name>tb-postgres</tb-postgres.docker.name>
<tb-cassandra.docker.name>tb-cassandra</tb-cassandra.docker.name> <tb-cassandra.docker.name>tb-cassandra</tb-cassandra.docker.name>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder> <pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
<pkg.upgradeVersion>3.3.0</pkg.upgradeVersion> <pkg.upgradeVersion>3.3.1</pkg.upgradeVersion>
</properties> </properties>
<dependencies> <dependencies>

9
ui-ngx/angular.json

@ -78,7 +78,8 @@
"node_modules/leaflet/dist/leaflet.css", "node_modules/leaflet/dist/leaflet.css",
"src/app/modules/home/components/widget/lib/maps/markers.scss", "src/app/modules/home/components/widget/lib/maps/markers.scss",
"node_modules/leaflet.markercluster/dist/MarkerCluster.css", "node_modules/leaflet.markercluster/dist/MarkerCluster.css",
"node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css" "node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css",
"node_modules/prismjs/themes/prism.css"
], ],
"stylePreprocessorOptions": { "stylePreprocessorOptions": {
"includePaths": [ "includePaths": [
@ -88,7 +89,11 @@
"scripts": [ "scripts": [
"node_modules/tinycolor2/dist/tinycolor-min.js", "node_modules/tinycolor2/dist/tinycolor-min.js",
"node_modules/split.js/dist/split.min.js", "node_modules/split.js/dist/split.min.js",
"node_modules/systemjs/dist/system.js" "node_modules/systemjs/dist/system.js",
"node_modules/marked/lib/marked.js",
"node_modules/prismjs/prism.js",
"node_modules/prismjs/components/prism-bash.min.js",
"node_modules/prismjs/components/prism-json.min.js"
], ],
"customWebpackConfig": { "customWebpackConfig": {
"path": "./extra-webpack.config.js" "path": "./extra-webpack.config.js"

1
ui-ngx/package.json

@ -72,6 +72,7 @@
"ngx-drag-drop": "^2.0.0", "ngx-drag-drop": "^2.0.0",
"ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master", "ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master",
"ngx-hm-carousel": "^2.0.0-rc.1", "ngx-hm-carousel": "^2.0.0-rc.1",
"ngx-markdown": "^10.1.1",
"ngx-sharebuttons": "^8.0.5", "ngx-sharebuttons": "^8.0.5",
"ngx-translate-messageformat-compiler": "^4.9.0", "ngx-translate-messageformat-compiler": "^4.9.0",
"objectpath": "^2.0.0", "objectpath": "^2.0.0",

18
ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts

@ -16,7 +16,7 @@
import { FormattedData, MapProviders, ReplaceInfo } from '@home/components/widget/lib/maps/map-models'; import { FormattedData, MapProviders, ReplaceInfo } from '@home/components/widget/lib/maps/map-models';
import { import {
createLabelFromDatasource, createLabelFromDatasource, deepClone,
hashCode, hashCode,
isDefined, isDefined,
isDefinedAndNotNull, isDefinedAndNotNull,
@ -343,6 +343,22 @@ export function parseData(input: DatasourceData[]): FormattedData[] {
}); });
} }
export function flatData(input: FormattedData[]): FormattedData {
let result: FormattedData = {} as FormattedData;
if (input.length) {
for (const toMerge of input) {
result = {...result, ...toMerge};
}
result.entityName = input[0].entityName;
result.entityId = input[0].entityId;
result.entityType = input[0].entityType;
result.$datasource = input[0].$datasource;
result.dsIndex = input[0].dsIndex;
result.deviceType = input[0].deviceType;
}
return result;
}
export function parseArray(input: DatasourceData[]): FormattedData[][] { export function parseArray(input: DatasourceData[]): FormattedData[][] {
return _(input).groupBy(el => el?.datasource?.entityName) return _(input).groupBy(el => el?.datasource?.entityName)
.values().value().map((entityArray) => .values().value().map((entityArray) =>

18
ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html

@ -0,0 +1,18 @@
<!--
Copyright © 2016-2021 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.
-->
<markdown [data]="markdownText" class="tb-markdown-view {{markdownClass}}" (click)="markdownClick($event)"></markdown>

130
ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.scss

@ -0,0 +1,130 @@
/**
* Copyright © 2016-2021 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.
*/
.tb-markdown-view {
display: block;
::ng-deep {
h1 {
font-size: 32px;
padding-right: 60px;
}
h1, h2, h3, h4, h5, h6 {
line-height: normal;
font-weight: 500;
margin-bottom: 30px;
padding-bottom: 10px;
border-bottom: 1px solid #ccc;
}
p {
font-size: 16px;
font-weight: 400;
line-height: 1.25em;
margin: 0;
}
p+p {
margin-top: 10px;
}
table {
width: 100%;
border: 1px solid #ccc;
border-spacing: 0;
margin-top: 30px;
margin-bottom: 30px;
}
thead {
background-color: #555;
color: #fff;
}
th, td {
font-size: .85em;
padding: 8px;
margin: 0;
text-align: left;
}
td[align=center], th[align=center] {
text-align: center;
}
td[align=right], th[align=right] {
text-align: right;
}
tr:nth-child(even) {
background-color: #f7f7f7;
}
code:not([class*=language-]) {
background: #f5f5f5;
border-radius: 2px;
color: #dd4a68;
padding: 2px 4px;
}
div.code-wrapper {
position: relative;
button.clipboard-btn {
cursor: pointer;
margin: 0;
border: 0;
outline: none;
position: absolute;
top: 5px;
right: 5px;
background: #fff;
box-shadow: 0 1px 8px 0 rgba(0,0,0,0.2), 0 3px 4px 0 rgba(0,0,0,0.14), 0 3px 3px -2px rgba(0,0,0,0.12);
border-radius: 5px;
opacity: 0;
transition: opacity .3s;
padding: 3px 6px;
line-height: 16px;
img {
width: 18px;
}
&:hover {
background: #f9f9f9;
}
&:active {
background-color: #ececec;
box-shadow: inset 1px -1px 4px 0px rgba(0,0,0,0.4);
}
}
&:hover {
button.clipboard-btn {
opacity: .85;
}
}
}
th, td {
div.code-wrapper {
display: inline-block;
button.clipboard-btn {
top: -5px;
right: -30px;
padding: 0 3px;
}
}
}
}
}

116
ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts

@ -0,0 +1,116 @@
///
/// Copyright © 2016-2021 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 { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DatasourceData } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import {
fillPattern, flatData,
parseData,
parseFunction,
processPattern,
safeExecute
} from '@home/components/widget/lib/maps/common-maps-utils';
import { FormattedData } from '@home/components/widget/lib/maps/map-models';
import { hashCode, isNotEmptyStr } from '@core/utils';
import cssjs from '@core/css/css';
interface MarkdownWidgetSettings {
markdownTextPattern: string;
useMarkdownTextFunction: boolean;
markdownTextFunction: string;
markdownCss: string;
}
type MarkdownTextFunction = (data: FormattedData[]) => string;
@Component({
selector: 'tb-markdown-widget ',
templateUrl: './markdown-widget.component.html',
styleUrls: ['./markdown-widget.component.scss']
})
export class MarkdownWidgetComponent extends PageComponent implements OnInit {
settings: MarkdownWidgetSettings;
markdownTextFunction: MarkdownTextFunction;
@Input()
ctx: WidgetContext;
markdownText: string;
markdownClass: string;
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef) {
super(store);
}
ngOnInit(): void {
this.ctx.$scope.markdownWidget = this;
this.settings = this.ctx.settings;
this.markdownTextFunction = this.settings.useMarkdownTextFunction ? parseFunction(this.settings.markdownTextFunction, ['data']) : null;
this.markdownClass = 'markdown-widget';
const cssString = this.settings.markdownCss;
if (isNotEmptyStr(cssString)) {
const cssParser = new cssjs();
cssParser.testMode = false;
this.markdownClass += '-' + hashCode(cssString);
cssParser.cssPreviewNamespace = 'tb-markdown-view.' + this.markdownClass;
cssParser.createStyleElement(this.markdownClass, cssString);
}
}
public onDataUpdated() {
let initialData: DatasourceData[];
if (this.ctx.data?.length) {
initialData = this.ctx.data;
} else if (this.ctx.datasources?.length) {
initialData = [
{
datasource: this.ctx.datasources[0],
dataKey: {
type: DataKeyType.attribute,
name: 'empty'
},
data: []
}
];
}
let markdownText: string;
if (initialData) {
const data = parseData(initialData);
markdownText = this.settings.useMarkdownTextFunction ?
safeExecute(this.markdownTextFunction, [data]) : this.settings.markdownTextPattern;
const allData = flatData(data);
const replaceInfo = processPattern(markdownText, allData);
markdownText = fillPattern(markdownText, replaceInfo, allData);
}
if (this.markdownText !== markdownText) {
this.markdownText = markdownText;
this.cd.detectChanges();
}
}
markdownClick($event: MouseEvent) {
this.ctx.actionsApi.elementClick($event);
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts

@ -101,7 +101,7 @@ export class QrCodeWidgetComponent extends PageComponent implements OnInit, Afte
const dataSourceData = data[0]; const dataSourceData = data[0];
const pattern = this.settings.useQrCodeTextFunction ? const pattern = this.settings.useQrCodeTextFunction ?
safeExecute(this.qrCodeTextFunction, [dataSourceData]) : this.settings.qrCodeTextPattern; safeExecute(this.qrCodeTextFunction, [dataSourceData]) : this.settings.qrCodeTextPattern;
const replaceInfo = processPattern(pattern, data); const replaceInfo = processPattern(pattern, dataSourceData);
qrCodeText = fillPattern(pattern, replaceInfo, dataSourceData); qrCodeText = fillPattern(pattern, replaceInfo, dataSourceData);
} }
this.updateQrCodeText(qrCodeText); this.updateQrCodeText(qrCodeText);

7
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -40,6 +40,7 @@ import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navig
import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-overview-widget.component'; import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-overview-widget.component';
import { JsonInputWidgetComponent } from '@home/components/widget/lib/json-input-widget.component'; import { JsonInputWidgetComponent } from '@home/components/widget/lib/json-input-widget.component';
import { QrCodeWidgetComponent } from '@home/components/widget/lib/qrcode-widget.component'; import { QrCodeWidgetComponent } from '@home/components/widget/lib/qrcode-widget.component';
import { MarkdownWidgetComponent } from '@home/components/widget/lib/markdown-widget.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -60,7 +61,8 @@ import { QrCodeWidgetComponent } from '@home/components/widget/lib/qrcode-widget
GatewayFormComponent, GatewayFormComponent,
NavigationCardsWidgetComponent, NavigationCardsWidgetComponent,
NavigationCardWidgetComponent, NavigationCardWidgetComponent,
QrCodeWidgetComponent QrCodeWidgetComponent,
MarkdownWidgetComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -83,7 +85,8 @@ import { QrCodeWidgetComponent } from '@home/components/widget/lib/qrcode-widget
GatewayFormComponent, GatewayFormComponent,
NavigationCardsWidgetComponent, NavigationCardsWidgetComponent,
NavigationCardWidgetComponent, NavigationCardWidgetComponent,
QrCodeWidgetComponent QrCodeWidgetComponent,
MarkdownWidgetComponent
], ],
providers: [ providers: [
CustomDialogService, CustomDialogService,

4
ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx

@ -159,8 +159,8 @@ class ThingsboardAceEditor extends React.Component<ThingsboardAceEditorProps, Th
<div className='json-form-ace-editor'> <div className='json-form-ace-editor'>
<div className='title-panel'> <div className='title-panel'>
<label>{this.props.mode}</label> <label>{this.props.mode}</label>
<Button style={ styles.tidyButtonStyle } { this.props.onTidy ? <Button style={ styles.tidyButtonStyle }
className='tidy-button' onClick={this.onTidy}>Tidy</Button> className='tidy-button' onClick={this.onTidy}>Tidy</Button> : null }
<Button style={ styles.tidyButtonStyle } <Button style={ styles.tidyButtonStyle }
className='tidy-button' onClick={this.onToggleFull}> className='tidy-button' onClick={this.onToggleFull}>
{this.state.isFull ? {this.state.isFull ?

33
ui-ngx/src/app/shared/components/json-form/react/json-form-markdown.tsx

@ -0,0 +1,33 @@
/*
* Copyright © 2016-2021 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 * as React from 'react';
import ThingsboardAceEditor from './json-form-ace-editor';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
class ThingsboardMarkdown extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
constructor(props) {
super(props);
}
render() {
return (
<ThingsboardAceEditor {...this.props} mode='markdown' {...this.state}></ThingsboardAceEditor>
);
}
}
export default ThingsboardMarkdown;

4
ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx

@ -38,6 +38,7 @@ import { JsonFormData, JsonFormProps, onChangeFn, OnColorClickFn, OnIconClickFn
import _ from 'lodash'; import _ from 'lodash';
import * as tinycolor_ from 'tinycolor2'; import * as tinycolor_ from 'tinycolor2';
import { GroupInfo } from '@shared/models/widget.models'; import { GroupInfo } from '@shared/models/widget.models';
import ThingsboardMarkdown from '@shared/components/json-form/react/json-form-markdown';
const tinycolor = tinycolor_; const tinycolor = tinycolor_;
@ -65,6 +66,7 @@ class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> {
json: ThingsboardJson, json: ThingsboardJson,
html: ThingsboardHtml, html: ThingsboardHtml,
css: ThingsboardCss, css: ThingsboardCss,
markdown: ThingsboardMarkdown,
color: ThingsboardColor, color: ThingsboardColor,
'rc-select': ThingsboardRcSelect, 'rc-select': ThingsboardRcSelect,
fieldset: ThingsboardFieldSet, fieldset: ThingsboardFieldSet,
@ -91,7 +93,7 @@ class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> {
} }
onIconClick(key: (string | number)[], val: string, onIconClick(key: (string | number)[], val: string,
iconSelectedFn: (icon: string) => void) { iconSelectedFn: (icon: string) => void) {
this.props.onIconClick(key, val, iconSelectedFn); this.props.onIconClick(key, val, iconSelectedFn);
} }

99
ui-ngx/src/app/shared/components/markdown.factory.ts

@ -0,0 +1,99 @@
///
/// Copyright © 2016-2021 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 { MarkedOptions, MarkedRenderer } from 'ngx-markdown';
export function markedOptionsFactory(): MarkedOptions {
const renderer = new MarkedRenderer();
const renderer2 = new MarkedRenderer();
const copyCodeBlock = '{:copy-code}';
let id = 1;
renderer.code = (code: string, language: string | undefined, isEscaped: boolean) => {
if (code.endsWith(copyCodeBlock)) {
code = code.substring(0, code.length - copyCodeBlock.length);
const content = renderer2.code(code, language, isEscaped);
id++;
return wrapCopyCode(id, content, code);
} else {
return renderer2.code(code, language, isEscaped);
}
};
renderer.tablecell = (content: string, flags: {
header: boolean;
align: 'center' | 'left' | 'right' | null;
}) => {
if (content.endsWith(copyCodeBlock)) {
content = content.substring(0, content.length - copyCodeBlock.length);
id++;
content = wrapCopyCode(id, content, content);
}
return renderer2.tablecell(content, flags);
};
return {
renderer,
headerIds: true,
gfm: true,
breaks: false,
pedantic: false,
smartLists: true,
smartypants: false,
};
}
function wrapCopyCode(id: number, content: string, code: string): string {
return '<div class="code-wrapper">' + content + '<span id="copyCodeId' + id + '" style="display: none;">' + code + '</span>' +
'<button id="copyCodeBtn' + id + '" onClick="markdownCopyCode(' + id + ')" ' +
'class="clipboard-btn"><img src="https://clipboardjs.com/assets/images/clippy.svg" alt="Copy to clipboard">' +
'</button></div>';
}
(window as any).markdownCopyCode = (id: number) => {
const text = $('#copyCodeId' + id).text();
navigator.clipboard.writeText(text).then(() => {
import('tooltipster').then(
() => {
const copyBtn = $('#copyCodeBtn' + id);
if (!copyBtn.hasClass('tooltipstered')) {
copyBtn.tooltipster(
{
content: 'Copied!',
theme: 'tooltipster-shadow',
delay: 0,
trigger: 'custom',
triggerClose: {
click: true,
tap: true,
scroll: true,
mouseleave: true
},
side: 'bottom',
distance: 12,
trackOrigin: true
}
);
}
const tooltip = copyBtn.tooltipster('instance');
tooltip.open();
}
);
});
};

15
ui-ngx/src/app/shared/shared.module.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { NgModule } from '@angular/core'; import { NgModule, SecurityContext } from '@angular/core';
import { CommonModule, DatePipe } from '@angular/common'; import { CommonModule, DatePipe } from '@angular/common';
import { FooterComponent } from '@shared/components/footer.component'; import { FooterComponent } from '@shared/components/footer.component';
import { LogoComponent } from '@shared/components/logo.component'; import { LogoComponent } from '@shared/components/logo.component';
@ -78,6 +78,7 @@ import { DatetimePeriodComponent } from '@shared/components/time/datetime-period
import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe'; import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe';
import { ClipboardModule } from 'ngx-clipboard'; import { ClipboardModule } from 'ngx-clipboard';
import { ValueInputComponent } from '@shared/components/value-input.component'; import { ValueInputComponent } from '@shared/components/value-input.component';
import { MarkdownModule, MarkedOptions } from 'ngx-markdown';
import { FullscreenDirective } from '@shared/components/fullscreen.directive'; import { FullscreenDirective } from '@shared/components/fullscreen.directive';
import { HighlightPipe } from '@shared/pipe/highlight.pipe'; import { HighlightPipe } from '@shared/pipe/highlight.pipe';
import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component'; import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component';
@ -145,6 +146,7 @@ import { OtaPackageAutocompleteComponent } from '@shared/components/ota-package/
import { MAT_DATE_LOCALE } from '@angular/material/core'; import { MAT_DATE_LOCALE } from '@angular/material/core';
import { CopyButtonComponent } from '@shared/components/button/copy-button.component'; import { CopyButtonComponent } from '@shared/components/button/copy-button.component';
import { TogglePasswordComponent } from '@shared/components/button/toggle-password.component'; import { TogglePasswordComponent } from '@shared/components/button/toggle-password.component';
import { markedOptionsFactory } from '@shared/components/markdown.factory';
@NgModule({ @NgModule({
providers: [ providers: [
@ -294,7 +296,15 @@ import { TogglePasswordComponent } from '@shared/components/button/toggle-passwo
NgxHmCarouselModule, NgxHmCarouselModule,
DndModule, DndModule,
NgxFlowModule, NgxFlowModule,
NgxFlowchartModule NgxFlowchartModule,
// ngx-markdown
MarkdownModule.forRoot({
sanitize: SecurityContext.NONE,
markedOptions: {
provide: MarkedOptions,
useFactory: markedOptionsFactory
}
})
], ],
exports: [ exports: [
FooterComponent, FooterComponent,
@ -386,6 +396,7 @@ import { TogglePasswordComponent } from '@shared/components/button/toggle-passwo
NgxHmCarouselModule, NgxHmCarouselModule,
DndModule, DndModule,
NgxFlowchartModule, NgxFlowchartModule,
MarkdownModule,
ConfirmDialogComponent, ConfirmDialogComponent,
AlertDialogComponent, AlertDialogComponent,
TodoDialogComponent, TodoDialogComponent,

39
ui-ngx/yarn.lock

@ -1817,6 +1817,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6"
integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q== integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==
"@types/marked@^1.1.0":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-1.2.2.tgz#1f858a0e690247ecf3b2eef576f98f86e8d960d4"
integrity sha512-wLfw1hnuuDYrFz97IzJja0pdVsC0oedtS4QsKH1/inyW9qkLQbXgMUqEQT0MVtUBx3twjWeInUfjQbhBVLECXw==
"@types/minimatch@*": "@types/minimatch@*":
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@ -4137,6 +4142,11 @@ emoji-regex@^8.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
emoji-toolkit@^6.0.1:
version "6.6.0"
resolved "https://registry.yarnpkg.com/emoji-toolkit/-/emoji-toolkit-6.6.0.tgz#e7287c43a96f940ec4c5428cd7100a40e57518f1"
integrity sha512-pEu0kow2p1N8zCKnn/L6H0F3rWUBB3P3hVjr/O5yl1fK7N9jU4vO4G7EFapC5Y3XwZLUCY0FZbOPyTkH+4V2eQ==
emojis-list@^3.0.0: emojis-list@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
@ -6126,6 +6136,13 @@ karma@~6.3.2:
ua-parser-js "^0.7.23" ua-parser-js "^0.7.23"
yargs "^16.1.1" yargs "^16.1.1"
katex@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.12.0.tgz#2fb1c665dbd2b043edcf8a1f5c555f46beaa0cb9"
integrity sha512-y+8btoc/CK70XqcHqjxiGWBOeIL8upbS0peTPXTvgrh21n1RiWWcIpSWM+4uXq+IAgNh9YYQWdc7LVDPDAEEAg==
dependencies:
commander "^2.19.0"
killable@^1.0.1: killable@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
@ -6425,6 +6442,11 @@ map-visit@^1.0.0:
dependencies: dependencies:
object-visit "^1.0.0" object-visit "^1.0.0"
marked@^1.1.0:
version "1.2.9"
resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.9.tgz#53786f8b05d4c01a2a5a76b7d1ec9943d29d72dc"
integrity sha512-H8lIX2SvyitGX+TRdtS06m1jHMijKN/XjfH6Ooii9fvxMlh8QdqBfBDkGUpMWH2kQNrtixjzYUa3SH8ROTgRRw==
material-design-icons@^3.0.1: material-design-icons@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/material-design-icons/-/material-design-icons-3.0.1.tgz#9a71c48747218ebca51e51a66da682038cdcb7bf" resolved "https://registry.yarnpkg.com/material-design-icons/-/material-design-icons-3.0.1.tgz#9a71c48747218ebca51e51a66da682038cdcb7bf"
@ -6877,6 +6899,18 @@ ngx-hm-carousel@^2.0.0-rc.1:
hammerjs "^2.0.8" hammerjs "^2.0.8"
resize-observer-polyfill "^1.5.1" resize-observer-polyfill "^1.5.1"
ngx-markdown@^10.1.1:
version "10.1.1"
resolved "https://registry.yarnpkg.com/ngx-markdown/-/ngx-markdown-10.1.1.tgz#17840c773db7ced4b18ccbf2e8cb06182e422de3"
integrity sha512-bUVgN6asb35d5U4xM5CNfo7pSpuwqJSdTgK0PhNZzLiaiyPIK2owtLF6sWGhxTThJu+LngJPjj4MQ+AFe/s8XQ==
dependencies:
"@types/marked" "^1.1.0"
emoji-toolkit "^6.0.1"
katex "^0.12.0"
marked "^1.1.0"
prismjs "^1.20.0"
tslib "^2.0.0"
ngx-sharebuttons@^8.0.5: ngx-sharebuttons@^8.0.5:
version "8.0.5" version "8.0.5"
resolved "https://registry.yarnpkg.com/ngx-sharebuttons/-/ngx-sharebuttons-8.0.5.tgz#49481fcb8bf9541747fd72093eca6f4777c1d371" resolved "https://registry.yarnpkg.com/ngx-sharebuttons/-/ngx-sharebuttons-8.0.5.tgz#49481fcb8bf9541747fd72093eca6f4777c1d371"
@ -7877,6 +7911,11 @@ pretty-bytes@^5.3.0:
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
prismjs@^1.20.0:
version "1.24.1"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.24.1.tgz#c4d7895c4d6500289482fa8936d9cdd192684036"
integrity sha512-mNPsedLuk90RVJioIky8ANZEwYm5w9LcvCXrxHlwf4fNVSn8jEipMybMkWUyyF0JhnC+C4VcOVSBuHRKs1L5Ow==
prismjs@^1.23.0: prismjs@^1.23.0:
version "1.23.0" version "1.23.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33"

Loading…
Cancel
Save