Commit e90e35c6780d9ff3ee6726b3530c811fccf99bf6

Authored by Artem Babak
1 parent 004df832

Device profiles: implementation of the protobuf editor for MQTT device transport configuration

@@ -74,35 +74,54 @@ @@ -74,35 +74,54 @@
74 </mat-error> 74 </mat-error>
75 </mat-form-field> 75 </mat-form-field>
76 <div *ngIf="protoPayloadType" fxLayout="column"> 76 <div *ngIf="protoPayloadType" fxLayout="column">
77 - <mat-form-field fxFlex>  
78 - <mat-label translate>device-profile.telemetry-proto-schema</mat-label>  
79 - <textarea matInput required formControlName="deviceTelemetryProtoSchema" rows="5"></textarea> 77 + <div>
  78 + <tb-protobuf-content
  79 + [disabled]="disabled"
  80 + fxFlex
  81 + formControlName="deviceTelemetryProtoSchema"
  82 + label="{{ 'device-profile.telemetry-proto-schema' | translate }}"
  83 + [fillHeight]="true">
  84 + </tb-protobuf-content>
80 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')"> 85 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')">
81 {{ 'device-profile.telemetry-proto-schema-required' | translate}} 86 {{ 'device-profile.telemetry-proto-schema-required' | translate}}
82 </mat-error> 87 </mat-error>
83 - </mat-form-field>  
84 - <mat-form-field fxFlex>  
85 - <mat-label translate>device-profile.attributes-proto-schema</mat-label>  
86 - <textarea matInput required formControlName="deviceAttributesProtoSchema" rows="5"></textarea> 88 + </div>
  89 + <div>
  90 + <tb-protobuf-content
  91 + [disabled]="disabled"
  92 + fxFlex
  93 + formControlName="deviceAttributesProtoSchema"
  94 + label="{{ 'device-profile.attributes-proto-schema' | translate }}"
  95 + [fillHeight]="true">
  96 + </tb-protobuf-content>
87 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')"> 97 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')">
88 {{ 'device-profile.attributes-proto-schema-required' | translate}} 98 {{ 'device-profile.attributes-proto-schema-required' | translate}}
89 </mat-error> 99 </mat-error>
90 - </mat-form-field>  
91 - <mat-form-field style="padding-bottom: 20px" fxFlex>  
92 - <mat-label translate>device-profile.rpc-request-proto-schema</mat-label>  
93 - <textarea matInput required formControlName="deviceRpcRequestProtoSchema" rows="5"></textarea> 100 + </div>
  101 + <div>
  102 + <tb-protobuf-content
  103 + [disabled]="disabled"
  104 + fxFlex
  105 + formControlName="deviceRpcRequestProtoSchema"
  106 + label="{{ 'device-profile.rpc-request-proto-schema' | translate }}"
  107 + [fillHeight]="true">
  108 + </tb-protobuf-content>
94 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')"> 109 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')">
95 - {{ 'device-profile.rpc-request-proto-schema-required' | translate}} 110 + {{ 'device-profile.rpc-request-proto-required' | translate}}
96 </mat-error> 111 </mat-error>
97 - <mat-hint class="tb-hint" translate>device-profile.rpc-request-proto-schema-hint</mat-hint>  
98 - </mat-form-field>  
99 - <mat-form-field fxFlex>  
100 - <mat-label translate>device-profile.rpc-response-proto-schema</mat-label>  
101 - <textarea matInput required formControlName="deviceRpcResponseProtoSchema" rows="5"></textarea> 112 + </div>
  113 + <div>
  114 + <tb-protobuf-content
  115 + [disabled]="disabled"
  116 + fxFlex
  117 + formControlName="deviceRpcResponseProtoSchema"
  118 + label="{{ 'device-profile.rpc-response-proto-schema' | translate }}"
  119 + [fillHeight]="true">
  120 + </tb-protobuf-content>
102 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')"> 121 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')">
103 {{ 'device-profile.rpc-response-proto-schema-required' | translate}} 122 {{ 'device-profile.rpc-response-proto-schema-required' | translate}}
104 </mat-error> 123 </mat-error>
105 - </mat-form-field> 124 + </div>
106 </div> 125 </div>
107 </div> 126 </div>
108 </fieldset> 127 </fieldset>
  1 +<div style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
  2 + tb-fullscreen
  3 + [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
  4 + <div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-protobuf-content-toolbar">
  5 + <label class="tb-title no-padding" [ngClass]="{'tb-error': !contentValid}">{{ label }}</label>
  6 + <span fxFlex></span>
  7 + <button type="button"
  8 + mat-button *ngIf="!readonly && !disabled" class="tidy" (click)="beautifyJSON()">
  9 + {{'js-func.tidy' | translate }}
  10 + </button>
  11 + <button type="button"
  12 + mat-button *ngIf="!readonly && !disabled" class="tidy" (click)="minifyJSON()">
  13 + {{'js-func.mini' | translate }}
  14 + </button>
  15 + <fieldset style="width: initial">
  16 + <div matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
  17 + matTooltipPosition="above"
  18 + style="border-radius: 50%"
  19 + (click)="fullscreen = !fullscreen">
  20 + <button type='button' mat-button mat-icon-button class="tb-mat-32">
  21 + <mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
  22 + </button>
  23 + </div>
  24 + </fieldset>
  25 + </div>
  26 + <div id="tb-protobuf-panel" tb-toast toastTarget="{{toastTargetId}}"
  27 + class="tb-protobuf-content-panel" fxLayout="column">
  28 + <div #protobufEditor id="tb-protobuf-input" [ngStyle]="editorStyle" [ngClass]="{'fill-height': fillHeight}"></div>
  29 + </div>
  30 +</div>
  1 +:host {
  2 + position: relative;
  3 +
  4 + .fill-height {
  5 + height: 100%;
  6 + }
  7 +}
  8 +
  9 +.tb-protobuf-content-toolbar {
  10 + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
  11 + align-items: center;
  12 + vertical-align: middle;
  13 + min-width: 32px;
  14 + min-height: 15px;
  15 + padding: 4px;
  16 + margin: 0;
  17 + font-size: .8rem;
  18 + line-height: 15px;
  19 + color: #7b7b7b;
  20 + background: rgba(220, 220, 220, .35);
  21 + &:not(:last-child) {
  22 + margin-right: 4px;
  23 + }
  24 + }
  25 +}
  26 +
  27 +.tb-protobuf-content-panel {
  28 + height: 100%;
  29 + margin-left: 15px;
  30 + border: 1px solid #c0c0c0;
  31 + overflow: auto;
  32 + resize: vertical;
  33 +
  34 + #tb-protobuf-input {
  35 + width: 100%;
  36 + min-width: 200px;
  37 + min-height: 100px;
  38 +
  39 + &:not(.fill-height) {
  40 + min-height: 200px;
  41 + }
  42 + }
  43 +}
  1 +import {
  2 + Component,
  3 + ElementRef,
  4 + forwardRef,
  5 + Input,
  6 + OnChanges,
  7 + OnDestroy,
  8 + OnInit,
  9 + SimpleChanges,
  10 + ViewChild
  11 +} from '@angular/core';
  12 +import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
  13 +import { Ace } from 'ace-builds';
  14 +import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
  15 +import { ResizeObserver } from '@juggle/resize-observer';
  16 +import { guid } from '@core/utils';
  17 +import { ContentType } from '@shared/models/constants';
  18 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { getAce } from '@shared/models/ace/ace.models';
  22 +import { beautifyJs } from '@shared/models/beautify.models';
  23 +import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
  24 +
  25 +@Component({
  26 + selector: 'tb-protobuf-content',
  27 + templateUrl: './protobuf-content.component.html',
  28 + styleUrls: ['./protobuf-content.component.scss'],
  29 + providers: [
  30 + {
  31 + provide: NG_VALUE_ACCESSOR,
  32 + useExisting: forwardRef(() => ProtobufContentComponent),
  33 + multi: true
  34 + },
  35 + {
  36 + provide: NG_VALIDATORS,
  37 + useExisting: forwardRef(() => ProtobufContentComponent),
  38 + multi: true,
  39 + }
  40 + ]
  41 +})
  42 +export class ProtobufContentComponent implements OnInit, ControlValueAccessor, Validator, OnChanges, OnDestroy {
  43 +
  44 + @ViewChild('protobufEditor', {static: true})
  45 + protobufEditorElmRef: ElementRef;
  46 +
  47 + private protobufEditor: Ace.Editor;
  48 + private editorsResizeCaf: CancelAnimationFrame;
  49 + private editorResize$: ResizeObserver;
  50 + private ignoreChange = false;
  51 +
  52 + toastTargetId = `protobufContentEditor-${guid()}`;
  53 +
  54 + @Input() label: string;
  55 +
  56 + contentType: ContentType = ContentType.TEXT;
  57 +
  58 + @Input() disabled: boolean;
  59 +
  60 + @Input() fillHeight: boolean;
  61 +
  62 + @Input() editorStyle: {[klass: string]: any};
  63 +
  64 + @Input() tbPlaceholder: string;
  65 +
  66 + private readonlyValue: boolean;
  67 + get readonly(): boolean {
  68 + return this.readonlyValue;
  69 + }
  70 + @Input()
  71 + set readonly(value: boolean) {
  72 + this.readonlyValue = coerceBooleanProperty(value);
  73 + }
  74 +
  75 + fullscreen = false;
  76 +
  77 + contentBody: string;
  78 +
  79 + contentValid: boolean;
  80 +
  81 + errorShowed = false;
  82 +
  83 + private propagateChange = null;
  84 +
  85 + constructor(public elementRef: ElementRef,
  86 + protected store: Store<AppState>,
  87 + private raf: RafService) {
  88 + }
  89 +
  90 + ngOnInit(): void {
  91 + const editorElement = this.protobufEditorElmRef.nativeElement;
  92 + let mode = 'protobuf';
  93 + let editorOptions: Partial<Ace.EditorOptions> = {
  94 + mode: `ace/mode/${mode}`,
  95 + showGutter: true,
  96 + showPrintMargin: false,
  97 + readOnly: this.disabled || this.readonly,
  98 + };
  99 +
  100 + const advancedOptions = {
  101 + enableSnippets: true,
  102 + enableBasicAutocompletion: true,
  103 + enableLiveAutocompletion: true,
  104 + autoScrollEditorIntoView: true
  105 + };
  106 +
  107 + editorOptions = {...editorOptions, ...advancedOptions};
  108 + getAce().subscribe(
  109 + (ace) => {
  110 + this.protobufEditor = ace.edit(editorElement, editorOptions);
  111 + this.protobufEditor.session.setUseWrapMode(true);
  112 + this.protobufEditor.setValue(this.contentBody ? this.contentBody : '', -1);
  113 + this.protobufEditor.setReadOnly(this.disabled || this.readonly);
  114 + this.protobufEditor.on('change', () => {
  115 + if (!this.ignoreChange) {
  116 + this.updateView();
  117 + }
  118 + });
  119 + this.editorResize$ = new ResizeObserver(() => {
  120 + this.onAceEditorResize();
  121 + });
  122 + this.editorResize$.observe(editorElement);
  123 + }
  124 + );
  125 + }
  126 +
  127 + ngOnDestroy(): void {
  128 + if (this.editorResize$) {
  129 + this.editorResize$.disconnect();
  130 + }
  131 + }
  132 +
  133 + private onAceEditorResize() {
  134 + if (this.editorsResizeCaf) {
  135 + this.editorsResizeCaf();
  136 + this.editorsResizeCaf = null;
  137 + }
  138 + this.editorsResizeCaf = this.raf.raf(() => {
  139 + this.protobufEditor.resize();
  140 + this.protobufEditor.renderer.updateFull();
  141 + });
  142 + }
  143 +
  144 + ngOnChanges(changes: SimpleChanges): void {
  145 + for (const propName of Object.keys(changes)) {
  146 + const change = changes[propName];
  147 + if (!change.firstChange && change.currentValue !== change.previousValue) {
  148 + if (propName === 'contentType') {
  149 + if (this.protobufEditor) {
  150 + let mode = 'protobuf';
  151 + this.protobufEditor.session.setMode(`ace/mode/${mode}`);
  152 + }
  153 + }
  154 + }
  155 + }
  156 + }
  157 +
  158 + registerOnChange(fn: any): void {
  159 + this.propagateChange = fn;
  160 + }
  161 +
  162 + registerOnTouched(fn: any): void {
  163 + }
  164 +
  165 + setDisabledState(isDisabled: boolean): void {
  166 + this.disabled = isDisabled;
  167 + if (this.protobufEditor) {
  168 + this.protobufEditor.setReadOnly(this.disabled || this.readonly);
  169 + }
  170 + }
  171 +
  172 + public validate(c: FormControl) {
  173 + return (this.contentValid) ? null : {
  174 + contentBody: {
  175 + valid: false,
  176 + },
  177 + };
  178 + }
  179 +
  180 + writeValue(value: string): void {
  181 + this.contentBody = value;
  182 + this.contentValid = true;
  183 + if (this.protobufEditor) {
  184 + this.ignoreChange = true;
  185 + this.protobufEditor.setValue(this.contentBody ? this.contentBody : '', -1);
  186 + this.ignoreChange = false;
  187 + }
  188 + }
  189 +
  190 + updateView() {
  191 + const editorValue = this.protobufEditor.getValue();
  192 + if (this.contentBody !== editorValue) {
  193 + this.contentBody = editorValue;
  194 + this.propagateChange(this.contentBody);
  195 + }
  196 + }
  197 +
  198 + beautifyJSON() {
  199 + beautifyJs(this.contentBody, {indent_size: 4, wrap_line_length: 60}).subscribe(
  200 + (res) => {
  201 + this.protobufEditor.setValue(res ? res : '', -1);
  202 + this.updateView();
  203 + }
  204 + );
  205 + }
  206 +
  207 + minifyJSON() {
  208 + const res = JSON.stringify(this.contentBody);
  209 + this.protobufEditor.setValue(res ? res : '', -1);
  210 + this.updateView();
  211 + }
  212 +
  213 + onFullscreen() {
  214 + if (this.protobufEditor) {
  215 + setTimeout(() => {
  216 + this.protobufEditor.resize();
  217 + }, 0);
  218 + }
  219 + }
  220 +
  221 +}
@@ -36,6 +36,7 @@ export function loadAceDependencies(): Observable<any> { @@ -36,6 +36,7 @@ export function loadAceDependencies(): Observable<any> {
36 aceObservables.push(from(import('ace-builds/src-noconflict/mode-text'))); 36 aceObservables.push(from(import('ace-builds/src-noconflict/mode-text')));
37 aceObservables.push(from(import('ace-builds/src-noconflict/mode-markdown'))); 37 aceObservables.push(from(import('ace-builds/src-noconflict/mode-markdown')));
38 aceObservables.push(from(import('ace-builds/src-noconflict/mode-html'))); 38 aceObservables.push(from(import('ace-builds/src-noconflict/mode-html')));
  39 + aceObservables.push(from(import('ace-builds/src-noconflict/mode-protobuf')));
39 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/java'))); 40 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/java')));
40 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/css'))); 41 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/css')));
41 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/json'))); 42 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/json')));
@@ -155,6 +155,7 @@ import { MarkedOptionsService } from '@shared/components/marked-options.service' @@ -155,6 +155,7 @@ import { MarkedOptionsService } from '@shared/components/marked-options.service'
155 import { TbPopoverService } from '@shared/components/popover.service'; 155 import { TbPopoverService } from '@shared/components/popover.service';
156 import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens'; 156 import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens';
157 import { TbMarkdownComponent } from '@shared/components/markdown.component'; 157 import { TbMarkdownComponent } from '@shared/components/markdown.component';
  158 +import { ProtobufContentComponent } from './components/protobuf-content.component';
158 159
159 export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { 160 export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
160 return markedOptionsService; 161 return markedOptionsService;
@@ -268,7 +269,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) @@ -268,7 +269,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
268 OtaPackageAutocompleteComponent, 269 OtaPackageAutocompleteComponent,
269 WidgetsBundleSearchComponent, 270 WidgetsBundleSearchComponent,
270 CopyButtonComponent, 271 CopyButtonComponent,
271 - TogglePasswordComponent 272 + TogglePasswordComponent,
  273 + ProtobufContentComponent
272 ], 274 ],
273 imports: [ 275 imports: [
274 CommonModule, 276 CommonModule,
@@ -458,7 +460,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) @@ -458,7 +460,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
458 OtaPackageAutocompleteComponent, 460 OtaPackageAutocompleteComponent,
459 WidgetsBundleSearchComponent, 461 WidgetsBundleSearchComponent,
460 CopyButtonComponent, 462 CopyButtonComponent,
461 - TogglePasswordComponent 463 + TogglePasswordComponent,
  464 + ProtobufContentComponent
462 ] 465 ]
463 }) 466 })
464 export class SharedModule { } 467 export class SharedModule { }