Commit 0c1d5b9cc061650f3a2fb9b069f5cf60087eaed9

Authored by Igor Kulikov
Committed by GitHub
2 parents b3534942 6f4d837a

Merge pull request #5450 from deaflynx/protobuf-ace-editor

[3.3.2] UI: Added ace editor for protobuf content
... ... @@ -47,35 +47,50 @@
47 47 </mat-error>
48 48 </mat-form-field>
49 49 <div *ngIf="protoPayloadType" fxLayout="column">
50   - <mat-form-field fxFlex>
51   - <mat-label translate>device-profile.telemetry-proto-schema</mat-label>
52   - <textarea matInput required formControlName="deviceTelemetryProtoSchema" rows="5"></textarea>
  50 + <ng-container>
  51 + <tb-protobuf-content
  52 + fxFlex
  53 + formControlName="deviceTelemetryProtoSchema"
  54 + label="{{ 'device-profile.telemetry-proto-schema' | translate }}"
  55 + [fillHeight]="true">
  56 + </tb-protobuf-content>
53 57 <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')">
54 58 {{ 'device-profile.telemetry-proto-schema-required' | translate}}
55 59 </mat-error>
56   - </mat-form-field>
57   - <mat-form-field fxFlex>
58   - <mat-label translate>device-profile.attributes-proto-schema</mat-label>
59   - <textarea matInput required formControlName="deviceAttributesProtoSchema" rows="5"></textarea>
  60 + </ng-container>
  61 + <ng-container>
  62 + <tb-protobuf-content
  63 + fxFlex
  64 + formControlName="deviceAttributesProtoSchema"
  65 + label="{{ 'device-profile.attributes-proto-schema' | translate }}"
  66 + [fillHeight]="true">
  67 + </tb-protobuf-content>
60 68 <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')">
61 69 {{ 'device-profile.attributes-proto-schema-required' | translate}}
62 70 </mat-error>
63   - </mat-form-field>
64   - <mat-form-field style="padding-bottom: 20px" fxFlex>
65   - <mat-label translate>device-profile.rpc-request-proto-schema</mat-label>
66   - <textarea matInput required formControlName="deviceRpcRequestProtoSchema" rows="5"></textarea>
  71 + </ng-container>
  72 + <ng-container>
  73 + <tb-protobuf-content
  74 + fxFlex
  75 + formControlName="deviceRpcRequestProtoSchema"
  76 + label="{{ 'device-profile.rpc-request-proto-schema' | translate }}"
  77 + [fillHeight]="true">
  78 + </tb-protobuf-content>
67 79 <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')">
68 80 {{ 'device-profile.rpc-request-proto-schema-required' | translate}}
69 81 </mat-error>
70   - <mat-hint class="tb-hint" translate>device-profile.rpc-request-proto-schema-hint</mat-hint>
71   - </mat-form-field>
72   - <mat-form-field fxFlex>
73   - <mat-label translate>device-profile.rpc-response-proto-schema</mat-label>
74   - <textarea matInput required formControlName="deviceRpcResponseProtoSchema" rows="5"></textarea>
  82 + </ng-container>
  83 + <ng-container>
  84 + <tb-protobuf-content
  85 + fxFlex
  86 + formControlName="deviceRpcResponseProtoSchema"
  87 + label="{{ 'device-profile.rpc-response-proto-schema' | translate }}"
  88 + [fillHeight]="true">
  89 + </tb-protobuf-content>
75 90 <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')">
76 91 {{ 'device-profile.rpc-response-proto-schema-required' | translate}}
77 92 </mat-error>
78   - </mat-form-field>
  93 + </ng-container>
79 94 </div>
80 95 </div>
81 96 </fieldset>
... ...
... ... @@ -86,35 +86,50 @@
86 86 </div>
87 87 </div>
88 88 <div *ngIf="protoPayloadType" fxLayout="column">
89   - <mat-form-field fxFlex>
90   - <mat-label translate>device-profile.telemetry-proto-schema</mat-label>
91   - <textarea matInput required formControlName="deviceTelemetryProtoSchema" rows="5"></textarea>
  89 + <ng-container>
  90 + <tb-protobuf-content
  91 + fxFlex
  92 + formControlName="deviceTelemetryProtoSchema"
  93 + label="{{ 'device-profile.telemetry-proto-schema' | translate }}"
  94 + [fillHeight]="true">
  95 + </tb-protobuf-content>
92 96 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')">
93 97 {{ 'device-profile.telemetry-proto-schema-required' | translate}}
94 98 </mat-error>
95   - </mat-form-field>
96   - <mat-form-field fxFlex>
97   - <mat-label translate>device-profile.attributes-proto-schema</mat-label>
98   - <textarea matInput required formControlName="deviceAttributesProtoSchema" rows="5"></textarea>
  99 + </ng-container>
  100 + <ng-container>
  101 + <tb-protobuf-content
  102 + fxFlex
  103 + formControlName="deviceAttributesProtoSchema"
  104 + label="{{ 'device-profile.attributes-proto-schema' | translate }}"
  105 + [fillHeight]="true">
  106 + </tb-protobuf-content>
99 107 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')">
100 108 {{ 'device-profile.attributes-proto-schema-required' | translate}}
101 109 </mat-error>
102   - </mat-form-field>
103   - <mat-form-field style="padding-bottom: 20px" fxFlex>
104   - <mat-label translate>device-profile.rpc-request-proto-schema</mat-label>
105   - <textarea matInput required formControlName="deviceRpcRequestProtoSchema" rows="5"></textarea>
  110 + </ng-container>
  111 + <ng-container>
  112 + <tb-protobuf-content
  113 + fxFlex
  114 + formControlName="deviceRpcRequestProtoSchema"
  115 + label="{{ 'device-profile.rpc-request-proto-schema' | translate }}"
  116 + [fillHeight]="true">
  117 + </tb-protobuf-content>
106 118 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')">
107 119 {{ 'device-profile.rpc-request-proto-schema-required' | translate}}
108 120 </mat-error>
109   - <mat-hint class="tb-hint" translate>device-profile.rpc-request-proto-schema-hint</mat-hint>
110   - </mat-form-field>
111   - <mat-form-field fxFlex>
112   - <mat-label translate>device-profile.rpc-response-proto-schema</mat-label>
113   - <textarea matInput required formControlName="deviceRpcResponseProtoSchema" rows="5"></textarea>
  121 + </ng-container>
  122 + <ng-container>
  123 + <tb-protobuf-content
  124 + fxFlex
  125 + formControlName="deviceRpcResponseProtoSchema"
  126 + label="{{ 'device-profile.rpc-response-proto-schema' | translate }}"
  127 + [fillHeight]="true">
  128 + </tb-protobuf-content>
114 129 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')">
115 130 {{ 'device-profile.rpc-response-proto-schema-required' | translate}}
116 131 </mat-error>
117   - </mat-form-field>
  132 + </ng-container>
118 133 </div>
119 134 </div>
120 135 </fieldset>
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
  19 + tb-fullscreen
  20 + [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
  21 + <div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-protobuf-content-toolbar">
  22 + <label class="tb-title no-padding">{{ label }}</label>
  23 + <span fxFlex></span>
  24 + <button type="button"
  25 + mat-button *ngIf="!readonly && !disabled" class="tidy" (click)="beautifyProtobuf()">
  26 + {{'js-func.tidy' | translate }}
  27 + </button>
  28 + <fieldset style="width: initial">
  29 + <div matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
  30 + matTooltipPosition="above"
  31 + style="border-radius: 50%"
  32 + (click)="fullscreen = !fullscreen">
  33 + <button type='button' mat-button mat-icon-button class="tb-mat-32">
  34 + <mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
  35 + </button>
  36 + </div>
  37 + </fieldset>
  38 + </div>
  39 + <div id="tb-protobuf-panel" tb-toast toastTarget="{{toastTargetId}}"
  40 + class="tb-protobuf-content-panel" fxLayout="column">
  41 + <div #protobufEditor id="tb-protobuf-input" [ngStyle]="editorStyle" [ngClass]="{'fill-height': fillHeight}"></div>
  42 + </div>
  43 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + position: relative;
  18 +
  19 + .fill-height {
  20 + height: 100%;
  21 + }
  22 +}
  23 +
  24 +.tb-protobuf-content-toolbar {
  25 + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
  26 + align-items: center;
  27 + vertical-align: middle;
  28 + min-width: 32px;
  29 + min-height: 15px;
  30 + padding: 4px;
  31 + margin: 0;
  32 + font-size: .8rem;
  33 + line-height: 15px;
  34 + color: #7b7b7b;
  35 + background: rgba(220, 220, 220, .35);
  36 + &:not(:last-child) {
  37 + margin-right: 4px;
  38 + }
  39 + }
  40 +}
  41 +
  42 +.tb-protobuf-content-panel {
  43 + height: 100%;
  44 + margin-left: 15px;
  45 + border: 1px solid #c0c0c0;
  46 +
  47 + #tb-protobuf-input {
  48 + width: 100%;
  49 + min-width: 200px;
  50 + min-height: 160px;
  51 + height: 100%;
  52 +
  53 + &:not(.fill-height) {
  54 + min-height: 200px;
  55 + }
  56 + }
  57 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + Component,
  19 + ElementRef,
  20 + forwardRef,
  21 + Input,
  22 + OnDestroy,
  23 + OnInit,
  24 + ViewChild
  25 +} from '@angular/core';
  26 +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
  27 +import { Ace } from 'ace-builds';
  28 +import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
  29 +import { ResizeObserver } from '@juggle/resize-observer';
  30 +import { guid } from '@core/utils';
  31 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  32 +import { Store } from '@ngrx/store';
  33 +import { AppState } from '@core/core.state';
  34 +import { getAce } from '@shared/models/ace/ace.models';
  35 +import { beautifyJs } from '@shared/models/beautify.models';
  36 +
  37 +@Component({
  38 + selector: 'tb-protobuf-content',
  39 + templateUrl: './protobuf-content.component.html',
  40 + styleUrls: ['./protobuf-content.component.scss'],
  41 + providers: [
  42 + {
  43 + provide: NG_VALUE_ACCESSOR,
  44 + useExisting: forwardRef(() => ProtobufContentComponent),
  45 + multi: true
  46 + }
  47 + ]
  48 +})
  49 +export class ProtobufContentComponent implements OnInit, ControlValueAccessor, OnDestroy {
  50 +
  51 + @ViewChild('protobufEditor', {static: true})
  52 + protobufEditorElmRef: ElementRef;
  53 +
  54 + private protobufEditor: Ace.Editor;
  55 + private editorsResizeCaf: CancelAnimationFrame;
  56 + private editorResize$: ResizeObserver;
  57 + private ignoreChange = false;
  58 +
  59 + toastTargetId = `protobufContentEditor-${guid()}`;
  60 +
  61 + @Input() label: string;
  62 +
  63 + @Input() disabled: boolean;
  64 +
  65 + @Input() fillHeight: boolean;
  66 +
  67 + @Input() editorStyle: {[klass: string]: any};
  68 +
  69 + @Input() tbPlaceholder: string;
  70 +
  71 + private readonlyValue: boolean;
  72 + get readonly(): boolean {
  73 + return this.readonlyValue;
  74 + }
  75 + @Input()
  76 + set readonly(value: boolean) {
  77 + this.readonlyValue = coerceBooleanProperty(value);
  78 + }
  79 +
  80 + fullscreen = false;
  81 +
  82 + contentBody: string;
  83 +
  84 + errorShowed = false;
  85 +
  86 + private propagateChange = null;
  87 +
  88 + constructor(public elementRef: ElementRef,
  89 + protected store: Store<AppState>,
  90 + private raf: RafService) {
  91 + }
  92 +
  93 + ngOnInit(): void {
  94 + const editorElement = this.protobufEditorElmRef.nativeElement;
  95 + let editorOptions: Partial<Ace.EditorOptions> = {
  96 + mode: `ace/mode/protobuf`,
  97 + showGutter: true,
  98 + showPrintMargin: false,
  99 + readOnly: this.disabled || this.readonly,
  100 + };
  101 +
  102 + const advancedOptions = {
  103 + enableSnippets: true,
  104 + enableBasicAutocompletion: true,
  105 + enableLiveAutocompletion: true
  106 + };
  107 +
  108 + editorOptions = {...editorOptions, ...advancedOptions};
  109 + getAce().subscribe(
  110 + (ace) => {
  111 + this.protobufEditor = ace.edit(editorElement, editorOptions);
  112 + this.protobufEditor.session.setUseWrapMode(true);
  113 + this.protobufEditor.setValue(this.contentBody ? this.contentBody : '', -1);
  114 + this.protobufEditor.setReadOnly(this.disabled || this.readonly);
  115 + this.protobufEditor.on('change', () => {
  116 + if (!this.ignoreChange) {
  117 + this.updateView();
  118 + }
  119 + });
  120 + this.editorResize$ = new ResizeObserver(() => {
  121 + this.onAceEditorResize();
  122 + });
  123 + this.editorResize$.observe(editorElement);
  124 + }
  125 + );
  126 + }
  127 +
  128 + ngOnDestroy(): void {
  129 + if (this.editorResize$) {
  130 + this.editorResize$.disconnect();
  131 + }
  132 + }
  133 +
  134 + registerOnChange(fn: any): void {
  135 + this.propagateChange = fn;
  136 + }
  137 +
  138 + registerOnTouched(fn: any): void {
  139 + }
  140 +
  141 + setDisabledState(isDisabled: boolean): void {
  142 + this.disabled = isDisabled;
  143 + if (this.protobufEditor) {
  144 + this.protobufEditor.setReadOnly(this.disabled || this.readonly);
  145 + }
  146 + }
  147 +
  148 + writeValue(value: string): void {
  149 + this.contentBody = value;
  150 + if (this.protobufEditor) {
  151 + this.ignoreChange = true;
  152 + this.protobufEditor.setValue(this.contentBody ? this.contentBody : '', -1);
  153 + this.ignoreChange = false;
  154 + }
  155 + }
  156 +
  157 + updateView() {
  158 + const editorValue = this.protobufEditor.getValue();
  159 + if (this.contentBody !== editorValue) {
  160 + this.contentBody = editorValue;
  161 + this.propagateChange(this.contentBody);
  162 + }
  163 + }
  164 +
  165 + beautifyProtobuf() {
  166 + beautifyJs(this.contentBody, {indent_size: 4, wrap_line_length: 60}).subscribe(
  167 + (res) => {
  168 + this.protobufEditor.setValue(res ? res : '', -1);
  169 + this.updateView();
  170 + }
  171 + );
  172 + }
  173 +
  174 + onFullscreen() {
  175 + if (this.protobufEditor) {
  176 + setTimeout(() => {
  177 + this.protobufEditor.resize();
  178 + }, 0);
  179 + }
  180 + }
  181 +
  182 + private onAceEditorResize() {
  183 + if (this.editorsResizeCaf) {
  184 + this.editorsResizeCaf();
  185 + this.editorsResizeCaf = null;
  186 + }
  187 + this.editorsResizeCaf = this.raf.raf(() => {
  188 + this.protobufEditor.resize();
  189 + this.protobufEditor.renderer.updateFull();
  190 + });
  191 + }
  192 +
  193 +}
... ...
... ... @@ -36,6 +36,8 @@ export function loadAceDependencies(): Observable<any> {
36 36 aceObservables.push(from(import('ace-builds/src-noconflict/mode-text')));
37 37 aceObservables.push(from(import('ace-builds/src-noconflict/mode-markdown')));
38 38 aceObservables.push(from(import('ace-builds/src-noconflict/mode-html')));
  39 + aceObservables.push(from(import('ace-builds/src-noconflict/mode-c_cpp')));
  40 + aceObservables.push(from(import('ace-builds/src-noconflict/mode-protobuf')));
39 41 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/java')));
40 42 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/css')));
41 43 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/json')));
... ... @@ -43,6 +45,8 @@ export function loadAceDependencies(): Observable<any> {
43 45 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/text')));
44 46 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/markdown')));
45 47 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/html')));
  48 + aceObservables.push(from(import('ace-builds/src-noconflict/snippets/c_cpp')));
  49 + aceObservables.push(from(import('ace-builds/src-noconflict/snippets/protobuf')));
46 50 aceObservables.push(from(import('ace-builds/src-noconflict/theme-textmate')));
47 51 aceObservables.push(from(import('ace-builds/src-noconflict/theme-github')));
48 52 return forkJoin(aceObservables).pipe(
... ...
... ... @@ -155,6 +155,7 @@ import { MarkedOptionsService } from '@shared/components/marked-options.service'
155 155 import { TbPopoverService } from '@shared/components/popover.service';
156 156 import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens';
157 157 import { TbMarkdownComponent } from '@shared/components/markdown.component';
  158 +import { ProtobufContentComponent } from './components/protobuf-content.component';
158 159
159 160 export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
160 161 return markedOptionsService;
... ... @@ -268,7 +269,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
268 269 OtaPackageAutocompleteComponent,
269 270 WidgetsBundleSearchComponent,
270 271 CopyButtonComponent,
271   - TogglePasswordComponent
  272 + TogglePasswordComponent,
  273 + ProtobufContentComponent
272 274 ],
273 275 imports: [
274 276 CommonModule,
... ... @@ -458,7 +460,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
458 460 OtaPackageAutocompleteComponent,
459 461 WidgetsBundleSearchComponent,
460 462 CopyButtonComponent,
461   - TogglePasswordComponent
  463 + TogglePasswordComponent,
  464 + ProtobufContentComponent
462 465 ]
463 466 })
464 467 export class SharedModule { }
... ...