Device profiles: implementation of the protobuf editor for MQTT device transport configuration
This commit is contained in:
parent
004df83266
commit
e90e35c678
@ -74,35 +74,54 @@
|
|||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div *ngIf="protoPayloadType" fxLayout="column">
|
<div *ngIf="protoPayloadType" fxLayout="column">
|
||||||
<mat-form-field fxFlex>
|
<div>
|
||||||
<mat-label translate>device-profile.telemetry-proto-schema</mat-label>
|
<tb-protobuf-content
|
||||||
<textarea matInput required formControlName="deviceTelemetryProtoSchema" rows="5"></textarea>
|
[disabled]="disabled"
|
||||||
|
fxFlex
|
||||||
|
formControlName="deviceTelemetryProtoSchema"
|
||||||
|
label="{{ 'device-profile.telemetry-proto-schema' | translate }}"
|
||||||
|
[fillHeight]="true">
|
||||||
|
</tb-protobuf-content>
|
||||||
<mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')">
|
<mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')">
|
||||||
{{ 'device-profile.telemetry-proto-schema-required' | translate}}
|
{{ 'device-profile.telemetry-proto-schema-required' | translate}}
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</div>
|
||||||
<mat-form-field fxFlex>
|
<div>
|
||||||
<mat-label translate>device-profile.attributes-proto-schema</mat-label>
|
<tb-protobuf-content
|
||||||
<textarea matInput required formControlName="deviceAttributesProtoSchema" rows="5"></textarea>
|
[disabled]="disabled"
|
||||||
|
fxFlex
|
||||||
|
formControlName="deviceAttributesProtoSchema"
|
||||||
|
label="{{ 'device-profile.attributes-proto-schema' | translate }}"
|
||||||
|
[fillHeight]="true">
|
||||||
|
</tb-protobuf-content>
|
||||||
<mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')">
|
<mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')">
|
||||||
{{ 'device-profile.attributes-proto-schema-required' | translate}}
|
{{ 'device-profile.attributes-proto-schema-required' | translate}}
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</div>
|
||||||
<mat-form-field style="padding-bottom: 20px" fxFlex>
|
<div>
|
||||||
<mat-label translate>device-profile.rpc-request-proto-schema</mat-label>
|
<tb-protobuf-content
|
||||||
<textarea matInput required formControlName="deviceRpcRequestProtoSchema" rows="5"></textarea>
|
[disabled]="disabled"
|
||||||
|
fxFlex
|
||||||
|
formControlName="deviceRpcRequestProtoSchema"
|
||||||
|
label="{{ 'device-profile.rpc-request-proto-schema' | translate }}"
|
||||||
|
[fillHeight]="true">
|
||||||
|
</tb-protobuf-content>
|
||||||
<mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')">
|
<mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')">
|
||||||
{{ 'device-profile.rpc-request-proto-schema-required' | translate}}
|
{{ 'device-profile.rpc-request-proto-required' | translate}}
|
||||||
</mat-error>
|
</mat-error>
|
||||||
<mat-hint class="tb-hint" translate>device-profile.rpc-request-proto-schema-hint</mat-hint>
|
</div>
|
||||||
</mat-form-field>
|
<div>
|
||||||
<mat-form-field fxFlex>
|
<tb-protobuf-content
|
||||||
<mat-label translate>device-profile.rpc-response-proto-schema</mat-label>
|
[disabled]="disabled"
|
||||||
<textarea matInput required formControlName="deviceRpcResponseProtoSchema" rows="5"></textarea>
|
fxFlex
|
||||||
|
formControlName="deviceRpcResponseProtoSchema"
|
||||||
|
label="{{ 'device-profile.rpc-response-proto-schema' | translate }}"
|
||||||
|
[fillHeight]="true">
|
||||||
|
</tb-protobuf-content>
|
||||||
<mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')">
|
<mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')">
|
||||||
{{ 'device-profile.rpc-response-proto-schema-required' | translate}}
|
{{ 'device-profile.rpc-response-proto-schema-required' | translate}}
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
<div style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
|
||||||
|
tb-fullscreen
|
||||||
|
[fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
|
||||||
|
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-protobuf-content-toolbar">
|
||||||
|
<label class="tb-title no-padding" [ngClass]="{'tb-error': !contentValid}">{{ label }}</label>
|
||||||
|
<span fxFlex></span>
|
||||||
|
<button type="button"
|
||||||
|
mat-button *ngIf="!readonly && !disabled" class="tidy" (click)="beautifyJSON()">
|
||||||
|
{{'js-func.tidy' | translate }}
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
mat-button *ngIf="!readonly && !disabled" class="tidy" (click)="minifyJSON()">
|
||||||
|
{{'js-func.mini' | translate }}
|
||||||
|
</button>
|
||||||
|
<fieldset style="width: initial">
|
||||||
|
<div matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
|
||||||
|
matTooltipPosition="above"
|
||||||
|
style="border-radius: 50%"
|
||||||
|
(click)="fullscreen = !fullscreen">
|
||||||
|
<button type='button' mat-button mat-icon-button class="tb-mat-32">
|
||||||
|
<mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<div id="tb-protobuf-panel" tb-toast toastTarget="{{toastTargetId}}"
|
||||||
|
class="tb-protobuf-content-panel" fxLayout="column">
|
||||||
|
<div #protobufEditor id="tb-protobuf-input" [ngStyle]="editorStyle" [ngClass]="{'fill-height': fillHeight}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
:host {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.fill-height {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-protobuf-content-toolbar {
|
||||||
|
button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
|
||||||
|
align-items: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
min-width: 32px;
|
||||||
|
min-height: 15px;
|
||||||
|
padding: 4px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: .8rem;
|
||||||
|
line-height: 15px;
|
||||||
|
color: #7b7b7b;
|
||||||
|
background: rgba(220, 220, 220, .35);
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-protobuf-content-panel {
|
||||||
|
height: 100%;
|
||||||
|
margin-left: 15px;
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
overflow: auto;
|
||||||
|
resize: vertical;
|
||||||
|
|
||||||
|
#tb-protobuf-input {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 200px;
|
||||||
|
min-height: 100px;
|
||||||
|
|
||||||
|
&:not(.fill-height) {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
221
ui-ngx/src/app/shared/components/protobuf-content.component.ts
Normal file
221
ui-ngx/src/app/shared/components/protobuf-content.component.ts
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
forwardRef,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
SimpleChanges,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
|
||||||
|
import { Ace } from 'ace-builds';
|
||||||
|
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
|
||||||
|
import { ResizeObserver } from '@juggle/resize-observer';
|
||||||
|
import { guid } from '@core/utils';
|
||||||
|
import { ContentType } from '@shared/models/constants';
|
||||||
|
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { AppState } from '@core/core.state';
|
||||||
|
import { getAce } from '@shared/models/ace/ace.models';
|
||||||
|
import { beautifyJs } from '@shared/models/beautify.models';
|
||||||
|
import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'tb-protobuf-content',
|
||||||
|
templateUrl: './protobuf-content.component.html',
|
||||||
|
styleUrls: ['./protobuf-content.component.scss'],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => ProtobufContentComponent),
|
||||||
|
multi: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: NG_VALIDATORS,
|
||||||
|
useExisting: forwardRef(() => ProtobufContentComponent),
|
||||||
|
multi: true,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ProtobufContentComponent implements OnInit, ControlValueAccessor, Validator, OnChanges, OnDestroy {
|
||||||
|
|
||||||
|
@ViewChild('protobufEditor', {static: true})
|
||||||
|
protobufEditorElmRef: ElementRef;
|
||||||
|
|
||||||
|
private protobufEditor: Ace.Editor;
|
||||||
|
private editorsResizeCaf: CancelAnimationFrame;
|
||||||
|
private editorResize$: ResizeObserver;
|
||||||
|
private ignoreChange = false;
|
||||||
|
|
||||||
|
toastTargetId = `protobufContentEditor-${guid()}`;
|
||||||
|
|
||||||
|
@Input() label: string;
|
||||||
|
|
||||||
|
contentType: ContentType = ContentType.TEXT;
|
||||||
|
|
||||||
|
@Input() disabled: boolean;
|
||||||
|
|
||||||
|
@Input() fillHeight: boolean;
|
||||||
|
|
||||||
|
@Input() editorStyle: {[klass: string]: any};
|
||||||
|
|
||||||
|
@Input() tbPlaceholder: string;
|
||||||
|
|
||||||
|
private readonlyValue: boolean;
|
||||||
|
get readonly(): boolean {
|
||||||
|
return this.readonlyValue;
|
||||||
|
}
|
||||||
|
@Input()
|
||||||
|
set readonly(value: boolean) {
|
||||||
|
this.readonlyValue = coerceBooleanProperty(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fullscreen = false;
|
||||||
|
|
||||||
|
contentBody: string;
|
||||||
|
|
||||||
|
contentValid: boolean;
|
||||||
|
|
||||||
|
errorShowed = false;
|
||||||
|
|
||||||
|
private propagateChange = null;
|
||||||
|
|
||||||
|
constructor(public elementRef: ElementRef,
|
||||||
|
protected store: Store<AppState>,
|
||||||
|
private raf: RafService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const editorElement = this.protobufEditorElmRef.nativeElement;
|
||||||
|
let mode = 'protobuf';
|
||||||
|
let editorOptions: Partial<Ace.EditorOptions> = {
|
||||||
|
mode: `ace/mode/${mode}`,
|
||||||
|
showGutter: true,
|
||||||
|
showPrintMargin: false,
|
||||||
|
readOnly: this.disabled || this.readonly,
|
||||||
|
};
|
||||||
|
|
||||||
|
const advancedOptions = {
|
||||||
|
enableSnippets: true,
|
||||||
|
enableBasicAutocompletion: true,
|
||||||
|
enableLiveAutocompletion: true,
|
||||||
|
autoScrollEditorIntoView: true
|
||||||
|
};
|
||||||
|
|
||||||
|
editorOptions = {...editorOptions, ...advancedOptions};
|
||||||
|
getAce().subscribe(
|
||||||
|
(ace) => {
|
||||||
|
this.protobufEditor = ace.edit(editorElement, editorOptions);
|
||||||
|
this.protobufEditor.session.setUseWrapMode(true);
|
||||||
|
this.protobufEditor.setValue(this.contentBody ? this.contentBody : '', -1);
|
||||||
|
this.protobufEditor.setReadOnly(this.disabled || this.readonly);
|
||||||
|
this.protobufEditor.on('change', () => {
|
||||||
|
if (!this.ignoreChange) {
|
||||||
|
this.updateView();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.editorResize$ = new ResizeObserver(() => {
|
||||||
|
this.onAceEditorResize();
|
||||||
|
});
|
||||||
|
this.editorResize$.observe(editorElement);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.editorResize$) {
|
||||||
|
this.editorResize$.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onAceEditorResize() {
|
||||||
|
if (this.editorsResizeCaf) {
|
||||||
|
this.editorsResizeCaf();
|
||||||
|
this.editorsResizeCaf = null;
|
||||||
|
}
|
||||||
|
this.editorsResizeCaf = this.raf.raf(() => {
|
||||||
|
this.protobufEditor.resize();
|
||||||
|
this.protobufEditor.renderer.updateFull();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
for (const propName of Object.keys(changes)) {
|
||||||
|
const change = changes[propName];
|
||||||
|
if (!change.firstChange && change.currentValue !== change.previousValue) {
|
||||||
|
if (propName === 'contentType') {
|
||||||
|
if (this.protobufEditor) {
|
||||||
|
let mode = 'protobuf';
|
||||||
|
this.protobufEditor.session.setMode(`ace/mode/${mode}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnChange(fn: any): void {
|
||||||
|
this.propagateChange = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnTouched(fn: any): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisabledState(isDisabled: boolean): void {
|
||||||
|
this.disabled = isDisabled;
|
||||||
|
if (this.protobufEditor) {
|
||||||
|
this.protobufEditor.setReadOnly(this.disabled || this.readonly);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public validate(c: FormControl) {
|
||||||
|
return (this.contentValid) ? null : {
|
||||||
|
contentBody: {
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
writeValue(value: string): void {
|
||||||
|
this.contentBody = value;
|
||||||
|
this.contentValid = true;
|
||||||
|
if (this.protobufEditor) {
|
||||||
|
this.ignoreChange = true;
|
||||||
|
this.protobufEditor.setValue(this.contentBody ? this.contentBody : '', -1);
|
||||||
|
this.ignoreChange = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateView() {
|
||||||
|
const editorValue = this.protobufEditor.getValue();
|
||||||
|
if (this.contentBody !== editorValue) {
|
||||||
|
this.contentBody = editorValue;
|
||||||
|
this.propagateChange(this.contentBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beautifyJSON() {
|
||||||
|
beautifyJs(this.contentBody, {indent_size: 4, wrap_line_length: 60}).subscribe(
|
||||||
|
(res) => {
|
||||||
|
this.protobufEditor.setValue(res ? res : '', -1);
|
||||||
|
this.updateView();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
minifyJSON() {
|
||||||
|
const res = JSON.stringify(this.contentBody);
|
||||||
|
this.protobufEditor.setValue(res ? res : '', -1);
|
||||||
|
this.updateView();
|
||||||
|
}
|
||||||
|
|
||||||
|
onFullscreen() {
|
||||||
|
if (this.protobufEditor) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.protobufEditor.resize();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -36,6 +36,7 @@ export function loadAceDependencies(): Observable<any> {
|
|||||||
aceObservables.push(from(import('ace-builds/src-noconflict/mode-text')));
|
aceObservables.push(from(import('ace-builds/src-noconflict/mode-text')));
|
||||||
aceObservables.push(from(import('ace-builds/src-noconflict/mode-markdown')));
|
aceObservables.push(from(import('ace-builds/src-noconflict/mode-markdown')));
|
||||||
aceObservables.push(from(import('ace-builds/src-noconflict/mode-html')));
|
aceObservables.push(from(import('ace-builds/src-noconflict/mode-html')));
|
||||||
|
aceObservables.push(from(import('ace-builds/src-noconflict/mode-protobuf')));
|
||||||
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/java')));
|
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/java')));
|
||||||
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/css')));
|
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/css')));
|
||||||
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/json')));
|
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/json')));
|
||||||
|
|||||||
@ -155,6 +155,7 @@ import { MarkedOptionsService } from '@shared/components/marked-options.service'
|
|||||||
import { TbPopoverService } from '@shared/components/popover.service';
|
import { TbPopoverService } from '@shared/components/popover.service';
|
||||||
import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens';
|
import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens';
|
||||||
import { TbMarkdownComponent } from '@shared/components/markdown.component';
|
import { TbMarkdownComponent } from '@shared/components/markdown.component';
|
||||||
|
import { ProtobufContentComponent } from './components/protobuf-content.component';
|
||||||
|
|
||||||
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
|
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
|
||||||
return markedOptionsService;
|
return markedOptionsService;
|
||||||
@ -268,7 +269,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
|||||||
OtaPackageAutocompleteComponent,
|
OtaPackageAutocompleteComponent,
|
||||||
WidgetsBundleSearchComponent,
|
WidgetsBundleSearchComponent,
|
||||||
CopyButtonComponent,
|
CopyButtonComponent,
|
||||||
TogglePasswordComponent
|
TogglePasswordComponent,
|
||||||
|
ProtobufContentComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@ -458,7 +460,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
|||||||
OtaPackageAutocompleteComponent,
|
OtaPackageAutocompleteComponent,
|
||||||
WidgetsBundleSearchComponent,
|
WidgetsBundleSearchComponent,
|
||||||
CopyButtonComponent,
|
CopyButtonComponent,
|
||||||
TogglePasswordComponent
|
TogglePasswordComponent,
|
||||||
|
ProtobufContentComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SharedModule { }
|
export class SharedModule { }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user