Browse Source

UI: SCADA symbol edit page.

pull/11391/head
Igor Kulikov 2 years ago
parent
commit
ecb11b6a3a
  1. 21
      ui-ngx/src/app/app.module.ts
  2. 39
      ui-ngx/src/app/core/http/image.service.ts
  3. 14
      ui-ngx/src/app/core/services/menu.service.ts
  4. 7
      ui-ngx/src/app/core/utils.ts
  5. 2
      ui-ngx/src/app/modules/home/components/details-panel.component.html
  6. 5
      ui-ngx/src/app/modules/home/components/details-panel.component.ts
  7. 50
      ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts
  8. 67
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  9. 4
      ui-ngx/src/app/modules/home/pages/home-pages.module.ts
  10. 42
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html
  11. 35
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.scss
  12. 102
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts
  13. 22
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts
  14. 34
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.module.ts
  15. 2
      ui-ngx/src/app/shared/components/image-input.component.html
  16. 38
      ui-ngx/src/app/shared/components/image-input.component.ts
  17. 15
      ui-ngx/src/app/shared/components/image/gallery-image-input.component.ts
  18. 1
      ui-ngx/src/app/shared/components/image/image-dialog.component.ts
  19. 1
      ui-ngx/src/app/shared/components/image/image-gallery-dialog.component.html
  20. 23
      ui-ngx/src/app/shared/components/image/image-gallery-dialog.component.ts
  21. 54
      ui-ngx/src/app/shared/components/image/image-gallery.component.html
  22. 71
      ui-ngx/src/app/shared/components/image/image-gallery.component.ts
  23. 10
      ui-ngx/src/app/shared/components/image/images-datasource.ts
  24. 15
      ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.ts
  25. 6
      ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html
  26. 9
      ui-ngx/src/app/shared/components/image/upload-image-dialog.component.ts
  27. 7
      ui-ngx/src/app/shared/models/resource.models.ts
  28. 20
      ui-ngx/src/assets/locale/locale.constant-en_US.json

21
ui-ngx/src/app/app.module.ts

@ -27,6 +27,23 @@ import { AppComponent } from './app.component';
import { DashboardRoutingModule } from '@modules/dashboard/dashboard-routing.module';
import { RouterModule, Routes } from '@angular/router';
import { DefaultUrlSerializer, UrlSerializer, UrlTree } from '@angular/router';
export default class TbUrlSerializer implements UrlSerializer {
private _defaultUrlSerializer: DefaultUrlSerializer = new DefaultUrlSerializer();
parse(url: string): UrlTree {
// Encode parentheses
url = url.replace(/\(/g, '%28').replace(/\)/g, '%29');
// Use the default serializer.
return this._defaultUrlSerializer.parse(url)
}
serialize(tree: UrlTree): string {
return this._defaultUrlSerializer.serialize(tree).replace(/%28/g, '(').replace(/%29/g, ')');
}
}
const routes: Routes = [
{ path: '**',
redirectTo: 'home'
@ -55,7 +72,9 @@ export class PageNotFoundRoutingModule { }
DashboardRoutingModule,
PageNotFoundRoutingModule
],
providers: [],
providers: [
{ provide: UrlSerializer, useClass: TbUrlSerializer }
],
bootstrap: [AppComponent]
})
export class AppModule { }

39
ui-ngx/src/app/core/http/image.service.ts

@ -25,11 +25,11 @@ import {
ImageResourceInfo,
imageResourceType,
ImageResourceType,
IMAGES_URL_PREFIX, isImageResourceUrl, ImageExportData, removeTbImagePrefix
IMAGES_URL_PREFIX, isImageResourceUrl, ImageExportData, removeTbImagePrefix, ResourceSubType
} from '@shared/models/resource.models';
import { catchError, map, switchMap } from 'rxjs/operators';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { blobToBase64 } from '@core/utils';
import { blobToBase64, blobToText } from '@core/utils';
import { ResourcesService } from '@core/services/resources.service';
@Injectable({
@ -46,13 +46,14 @@ export class ImageService {
) {
}
public uploadImage(file: File, title: string, config?: RequestConfig): Observable<ImageResourceInfo> {
public uploadImage(file: File, title: string, imageSubType: ResourceSubType, config?: RequestConfig): Observable<ImageResourceInfo> {
if (!config) {
config = {};
}
const formData = new FormData();
formData.append('file', file);
formData.append('title', title);
formData.append('imageSubType', imageSubType);
return this.http.post<ImageResourceInfo>('/api/image', formData,
defaultHttpUploadOptions(config.ignoreLoading, config.ignoreErrors, config.resendRequest));
}
@ -81,9 +82,10 @@ export class ImageService {
imageInfo, defaultHttpOptionsFromConfig(config));
}
public getImages(pageLink: PageLink, includeSystemImages = false, config?: RequestConfig): Observable<PageData<ImageResourceInfo>> {
public getImages(pageLink: PageLink, imageSubType: ResourceSubType,
includeSystemImages = false, config?: RequestConfig): Observable<PageData<ImageResourceInfo>> {
return this.http.get<PageData<ImageResourceInfo>>(
`${IMAGES_URL_PREFIX}${pageLink.toQuery()}&includeSystemImages=${includeSystemImages}`,
`${IMAGES_URL_PREFIX}${pageLink.toQuery()}&imageSubType=${imageSubType}&includeSystemImages=${includeSystemImages}`,
defaultHttpOptionsFromConfig(config));
}
@ -130,6 +132,33 @@ export class ImageService {
);
}
public getImageString(imageUrl: string): Observable<string> {
imageUrl = removeTbImagePrefix(imageUrl);
let request: ReplaySubject<Blob>;
if (this.imagesLoading[imageUrl]) {
request = this.imagesLoading[imageUrl];
} else {
request = new ReplaySubject<Blob>(1);
this.imagesLoading[imageUrl] = request;
const options = defaultHttpOptionsFromConfig({ignoreLoading: true, ignoreErrors: true});
this.http.get(imageUrl, {...options, ...{ responseType: 'blob' } }).subscribe({
next: (value) => {
request.next(value);
request.complete();
},
error: err => {
request.error(err);
},
complete: () => {
delete this.imagesLoading[imageUrl];
}
});
}
return request.pipe(
switchMap(val => blobToText(val))
);
}
public resolveImageUrl(imageUrl: string, preview = false, asString = false, emptyUrl = NO_IMAGE_DATA_URI): Observable<SafeUrl | string> {
imageUrl = removeTbImagePrefix(imageUrl);
if (isImageResourceUrl(imageUrl)) {

14
ui-ngx/src/app/core/services/menu.service.ts

@ -150,6 +150,13 @@ export class MenuService {
path: '/resources/images',
icon: 'filter'
},
{
id: 'scada_symbols',
name: 'scada.symbols',
type: 'link',
path: '/resources/scada-symbols',
icon: 'precision_manufacturing'
},
{
id: 'resources_library',
name: 'resource.resources-library',
@ -541,6 +548,13 @@ export class MenuService {
path: '/resources/images',
icon: 'filter'
},
{
id: 'scada_symbols',
name: 'scada.symbols',
type: 'link',
path: '/resources/scada-symbols',
icon: 'precision_manufacturing'
},
{
id: 'resources_library',
name: 'resource.resources-library',

7
ui-ngx/src/app/core/utils.ts

@ -217,6 +217,13 @@ export const blobToBase64 = (blob: Blob): Observable<string> => from(new Promise
}
));
export const blobToText = (blob: Blob): Observable<string> => from(new Promise<string>((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.readAsText(blob);
}
));
const scrollRegex = /(auto|scroll)/;
function parentNodes(node: Node, nodes: Node[]): Node[] {

2
ui-ngx/src/app/modules/home/components/details-panel.component.html

@ -40,7 +40,7 @@
<mat-icon class="material-icons">search</mat-icon>
</button>
<ng-content select=".details-buttons"></ng-content>
<button mat-icon-button (click)="onCloseDetails()">
<button *ngIf="showCloseDetails" mat-icon-button (click)="onCloseDetails()">
<mat-icon class="material-icons">close</mat-icon>
</button>
</div>

5
ui-ngx/src/app/modules/home/components/details-panel.component.ts

@ -28,6 +28,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UntypedFormGroup } from '@angular/forms';
import { Subscription } from 'rxjs';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-details-panel',
@ -44,6 +45,10 @@ export class DetailsPanelComponent extends PageComponent implements OnDestroy {
@Input() isShowSearch = false;
@Input() backgroundColor = '#FFF';
@Input()
@coerceBoolean()
showCloseDetails = true;
private theFormValue: UntypedFormGroup;
private formSubscription: Subscription = null;

50
ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts

@ -40,6 +40,7 @@ import { ColorProcessor, constantColor, Font } from '@shared/models/widget-setti
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service';
import { WidgetAction, WidgetActionType } from '@shared/models/widget.models';
import { ResizeObserver } from '@juggle/resize-observer';
export interface IotSvgApi {
formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined;
@ -248,6 +249,55 @@ export type IotSvgObjectSettings = {[id: string]: any};
const parseError = (ctx: WidgetContext, err: any): string =>
ctx.$injector.get(UtilsService).parseException(err).message || 'Unknown Error';
export class IotSvgEditObject {
private svgShape: Svg;
private box: Box;
private shapeResize$: ResizeObserver;
constructor(private rootElement: HTMLElement) {
this.shapeResize$ = new ResizeObserver(() => {
this.resize();
});
this.shapeResize$.observe(this.rootElement);
}
public setContent(svgContent: string) {
if (this.svgShape) {
this.svgShape.remove();
}
const doc: XMLDocument = new DOMParser().parseFromString(svgContent, 'image/svg+xml');
this.svgShape = SVG().svg(doc.documentElement.innerHTML);
this.svgShape.node.style.overflow = 'visible';
this.svgShape.node.style['user-select'] = 'none';
this.box = this.svgShape.bbox();
this.svgShape.size(this.box.width, this.box.height);
this.svgShape.addTo(this.rootElement);
this.resize();
}
public destroy() {
if (this.shapeResize$) {
this.shapeResize$.disconnect();
}
}
private resize() {
if (this.svgShape) {
const targetWidth = this.rootElement.getBoundingClientRect().width;
const targetHeight = this.rootElement.getBoundingClientRect().height;
let scale: number;
if (targetWidth < targetHeight) {
scale = targetWidth / this.box.width;
} else {
scale = targetHeight / this.box.height;
}
this.svgShape.node.style.transform = `scale(${scale})`;
}
}
}
export class IotSvgObject {
private metadata: IotSvgMetadata;

67
ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts

@ -14,8 +14,8 @@
/// limitations under the License.
///
import { Injectable, NgModule } from '@angular/core';
import { Resolve, RouterModule, Routes } from '@angular/router';
import { inject, Injectable, NgModule } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, ResolveFn, RouterModule, RouterStateSnapshot, Routes } from '@angular/router';
import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
@ -23,7 +23,7 @@ import { Authority } from '@shared/models/authority.enum';
import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component';
import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component';
import { OAuth2SettingsComponent } from '@home/pages/admin/oauth2-settings.component';
import { Observable } from 'rxjs';
import { forkJoin, Observable } from 'rxjs';
import { OAuth2Service } from '@core/http/oauth2.service';
import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
@ -31,7 +31,7 @@ import { EntitiesTableComponent } from '@home/components/entity/entities-table.c
import { ResourcesLibraryTableConfigResolver } from '@home/pages/admin/resource/resources-library-table-config.resolve';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb';
import { QueuesTableConfigResolver } from '@home/pages/admin/queue/queues-table-config.resolver';
import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component';
import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component';
@ -40,6 +40,10 @@ import { widgetsLibraryRoutes } from '@home/pages/widget/widget-library-routing.
import { RouterTabsComponent } from '@home/components/router-tabs.component';
import { auditLogsRoutes } from '@home/pages/audit-log/audit-log-routing.module';
import { ImageGalleryComponent } from '@shared/components/image/image-gallery.component';
import { ImageResourceType, IMAGES_URL_PREFIX, ResourceSubType } from '@shared/models/resource.models';
import { ScadaSymbolComponent } from '@home/pages/scada-symbol/scada-symbol.component';
import { ImageService } from '@core/http/image.service';
import { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol.models';
@Injectable()
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@ -52,6 +56,22 @@ export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
}
}
export const scadaSymbolResolver: ResolveFn<ScadaSymbolData> =
(route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
imageService = inject(ImageService)) => {
const type: ImageResourceType = route.params.type;
const key = decodeURIComponent(route.params.key);
return forkJoin({
imageResource: imageService.getImageInfo(type, key),
svgContent: imageService.getImageString(`${IMAGES_URL_PREFIX}/${type}/${encodeURIComponent(key)}`)
});
};
export const scadaSymbolBreadcumbLabelFunction: BreadCrumbLabelFunction<ScadaSymbolComponent>
= ((route, translate, component) =>
component.symbolData?.imageResource?.title);
const routes: Routes = [
{
path: 'resources',
@ -87,8 +107,45 @@ const routes: Routes = [
data: {
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'image.gallery',
},
imageSubType: ResourceSubType.IMAGE
}
}
]
},
{
path: 'scada-symbols',
data: {
breadcrumb: {
label: 'scada.symbols',
icon: 'precision_manufacturing'
}
},
children: [
{
path: '',
component: ImageGalleryComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'scada.symbols',
imageSubType: ResourceSubType.IOT_SVG
}
},
{
path: ':type/:key',
component: ScadaSymbolComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: scadaSymbolBreadcumbLabelFunction,
icon: 'precision_manufacturing'
} as BreadCrumbConfig<ScadaSymbolComponent>,
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'scada.symbol'
},
resolve: {
symbolData: scadaSymbolResolver
}
},
]
},
{

4
ui-ngx/src/app/modules/home/pages/home-pages.module.ts

@ -43,6 +43,7 @@ import { EntitiesModule } from '@home/pages/entities/entities.module';
import { FeaturesModule } from '@home/pages/features/features.module';
import { NotificationModule } from '@home/pages/notification/notification.module';
import { AccountModule } from '@home/pages/account/account.module';
import { ScadaSymbolModule } from '@home/pages/scada-symbol/scada-symbol.module';
@NgModule({
exports: [
@ -72,7 +73,8 @@ import { AccountModule } from '@home/pages/account/account.module';
OtaUpdateModule,
UserModule,
VcModule,
AccountModule
AccountModule,
ScadaSymbolModule
]
})
export class HomePagesModule { }

42
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.html

@ -0,0 +1,42 @@
<!--
Copyright © 2016-2024 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.
-->
<div class="tb-scada-symbol mat-content" fxLayout="column">
<section class="tb-scada-symbol-container" fxFlex fxLayout="column">
<div class="tb-scada-symbol-layout" fxFlex fxLayout="row">
<mat-drawer-container style="width: 100%; height: 100%;">
<mat-drawer class="tb-scada-symbol-details-drawer mat-elevation-z4"
disableClose="true"
mode="side"
opened
position="end">
<tb-details-panel fxFlex
[isReadOnly]="true"
[showCloseDetails]="false"
headerTitle="{{symbolData?.imageResource?.title}}">
<div>
{{ metadata | json }}
</div>
</tb-details-panel>
</mat-drawer>
<mat-drawer-content class="tb-scada-symbol-content">
<div class="tb-scada-symbol-shape" #iotSvgShape></div>
</mat-drawer-content>
</mat-drawer-container>
</div>
</section>
</div>

35
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.scss

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2024 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-scada-symbol {
width: 100%;
height: 100%;
.tb-scada-symbol-details-drawer {
width: 50%;
}
.tb-scada-symbol-content {
flex: 1;
min-width: 0;
min-height: 0;
background: #fff;
.tb-scada-symbol-shape {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
}

102
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts

@ -0,0 +1,102 @@
///
/// Copyright © 2016-2024 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 {
AfterViewInit,
Component,
ElementRef,
HostBinding,
OnDestroy,
OnInit,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ActivatedRoute, Router } from '@angular/router';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol.models';
import {
IotSvgEditObject,
IotSvgMetadata,
parseIotSvgMetadataFromContent
} from '@home/components/widget/lib/svg/iot-svg.models';
@Component({
selector: 'tb-scada-symbol',
templateUrl: './scada-symbol.component.html',
styleUrls: ['./scada-symbol.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ScadaSymbolComponent extends PageComponent implements OnInit, OnDestroy, AfterViewInit {
@HostBinding('style.width') width = '100%';
@HostBinding('style.height') height = '100%';
@ViewChild('iotSvgShape', {static: false})
iotSvgShape: ElementRef<HTMLElement>;
symbolData: ScadaSymbolData;
iotSvgObjectEdit: IotSvgEditObject;
metadata: IotSvgMetadata;
private destroy$ = new Subject<void>();
constructor(protected store: Store<AppState>,
private router: Router,
private route: ActivatedRoute) {
super(store);
this.route.data.pipe(
takeUntil(this.destroy$)
).subscribe(
() => {
this.reset();
this.init();
}
);
}
ngOnInit(): void {
}
ngAfterViewInit() {
this.iotSvgObjectEdit = new IotSvgEditObject(this.iotSvgShape.nativeElement);
if (this.symbolData) {
this.iotSvgObjectEdit.setContent(this.symbolData.svgContent);
}
}
ngOnDestroy() {
super.ngOnDestroy();
this.destroy$.next();
this.destroy$.complete();
}
private reset(): void {
}
private init() {
this.symbolData = this.route.snapshot.data.symbolData;
this.metadata = parseIotSvgMetadataFromContent(this.symbolData.svgContent);
if (this.iotSvgObjectEdit) {
this.iotSvgObjectEdit.setContent(this.symbolData.svgContent);
}
}
}

22
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.models.ts

@ -0,0 +1,22 @@
///
/// Copyright © 2016-2024 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 { ImageResourceInfo } from '@shared/models/resource.models';
export interface ScadaSymbolData {
imageResource: ImageResourceInfo;
svgContent: string;
}

34
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.module.ts

@ -0,0 +1,34 @@
///
/// Copyright © 2016-2024 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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@app/shared/shared.module';
import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { ScadaSymbolComponent } from '@home/pages/scada-symbol/scada-symbol.component';
@NgModule({
declarations:
[
ScadaSymbolComponent
],
imports: [
CommonModule,
SharedModule,
HomeComponentsModule
]
})
export class ScadaSymbolModule { }

2
ui-ngx/src/app/shared/components/image-input.component.html

@ -47,7 +47,7 @@
<button type="button" mat-button color="primary" class="browse-file">
<label for="{{inputId}}">{{'image-input.browse' | translate}}</label>
</button>
<input class="file-input" flowButton type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: 'image/*'}" id="{{inputId}}">
<input class="file-input" flowButton type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="{{inputId}}">
</div>
</div>
</div>

38
ui-ngx/src/app/shared/components/image-input.component.ts

@ -53,6 +53,9 @@ import { ImagePipe } from '@shared/pipe/image.pipe';
})
export class ImageInputComponent extends PageComponent implements AfterViewInit, OnDestroy, ControlValueAccessor {
@Input()
accept = 'image/*';
@Input()
label: string;
@ -85,6 +88,9 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
@Input()
inputId = this.utils.guid();
@Input()
allowedExtensions: string;
@Input()
@coerceBoolean()
processImageApiLink = false;
@ -141,17 +147,19 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
);
return false;
}
const reader = new FileReader();
reader.onload = (_loadEvent) => {
if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) {
this.imageUrl = reader.result;
this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl);
this.file = file;
this.fileName = fileName;
this.updateModel();
}
};
reader.readAsDataURL(file);
if (this.filterFile(flowFile)) {
const reader = new FileReader();
reader.onload = (_loadEvent) => {
if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) {
this.imageUrl = reader.result;
this.safeImageUrl = this.sanitizer.bypassSecurityTrustUrl(this.imageUrl);
this.file = file;
this.fileName = fileName;
this.updateModel();
}
};
reader.readAsDataURL(file);
}
}
});
}
@ -198,6 +206,14 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
this.fileNameChanged.emit(this.fileName);
}
private filterFile(file: flowjs.FlowFile): boolean {
if (this.allowedExtensions) {
return this.allowedExtensions.split(',').indexOf(file.getExtension()) > -1;
} else {
return true;
}
}
clearImage() {
this.imageUrl = null;
this.safeImageUrl = null;

15
ui-ngx/src/app/shared/components/image/gallery-image-input.component.ts

@ -26,11 +26,15 @@ import {
isBase64DataImageUrl,
isImageResourceUrl,
prependTbImagePrefix,
removeTbImagePrefix
removeTbImagePrefix,
ResourceSubType
} from '@shared/models/resource.models';
import { ImageService } from '@core/http/image.service';
import { MatDialog } from '@angular/material/dialog';
import { ImageGalleryDialogComponent } from '@shared/components/image/image-gallery-dialog.component';
import {
ImageGalleryDialogComponent,
ImageGalleryDialogData
} from '@shared/components/image/image-gallery-dialog.component';
export enum ImageLinkType {
none = 'none',
@ -188,11 +192,14 @@ export class GalleryImageInputComponent extends PageComponent implements OnInit,
if ($event) {
$event.stopPropagation();
}
this.dialog.open<ImageGalleryDialogComponent, any,
this.dialog.open<ImageGalleryDialogComponent, ImageGalleryDialogData,
ImageResourceInfo>(ImageGalleryDialogComponent, {
autoFocus: false,
disableClose: false,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
imageSubType: ResourceSubType.IMAGE
}
}).afterClosed().subscribe((image) => {
if (image) {
this.linkType = ImageLinkType.resource;

1
ui-ngx/src/app/shared/components/image/image-dialog.component.ts

@ -146,6 +146,7 @@ export class ImageDialogComponent extends
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
imageSubType: this.image.resourceSubType,
image: this.image
}
}).afterClosed().subscribe((result) => {

1
ui-ngx/src/app/shared/components/image/image-gallery-dialog.component.html

@ -24,6 +24,7 @@
</button>
<tb-image-gallery
pageMode="false"
[imageSubType]="data.imageSubType"
dialogMode
mode="grid"
selectionMode

23
ui-ngx/src/app/shared/components/image/image-gallery-dialog.component.ts

@ -14,23 +14,17 @@
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { ImageService } from '@core/http/image.service';
import { ImageResourceInfo, imageResourceType } from '@shared/models/resource.models';
import {
UploadImageDialogComponent,
UploadImageDialogData
} from '@shared/components/image/upload-image-dialog.component';
import { UrlHolder } from '@shared/pipe/image.pipe';
import { ImportExportService } from '@shared/import-export/import-export.service';
import { EmbedImageDialogComponent, EmbedImageDialogData } from '@shared/components/image/embed-image-dialog.component';
import { ImageResourceInfo, ResourceSubType } from '@shared/models/resource.models';
export interface ImageGalleryDialogData {
imageSubType: ResourceSubType;
}
@Component({
selector: 'tb-image-gallery-dialog',
@ -42,8 +36,7 @@ export class ImageGalleryDialogComponent extends
constructor(protected store: Store<AppState>,
protected router: Router,
private imageService: ImageService,
private dialog: MatDialog,
@Inject(MAT_DIALOG_DATA) public data: ImageGalleryDialogData,
public dialogRef: MatDialogRef<ImageGalleryDialogComponent, ImageResourceInfo>) {
super(store, router, dialogRef);
}

54
ui-ngx/src/app/shared/components/image/image-gallery.component.html

@ -22,7 +22,7 @@
[fxShow]="!textSearchMode && (mode === 'grid' || dataSource?.selection.isEmpty())" [ngClass.lt-lg]="{'multi-row': !isSysAdmin}">
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<div class="mat-toolbar-tools">
<span fxHide fxShow.gt-sm class="tb-images-title" translate>image.gallery</span>
<span fxHide fxShow.gt-sm class="tb-images-title">{{ (isScada ? 'scada.symbols' : 'image.gallery') | translate }}</span>
<div class="tb-images-view-type-toolbar"
fxLayout="row"
fxLayoutAlign="start center">
@ -46,7 +46,7 @@
<mat-slide-toggle *ngIf="!isSysAdmin"
fxHide fxShow.gt-md
[ngModel]="includeSystemImages"
(ngModelChange)="includeSystemImagesChanged($event)">{{ 'image.include-system-images' | translate }}</mat-slide-toggle>
(ngModelChange)="includeSystemImagesChanged($event)">{{ (isScada ? 'scada.include-system-symbols' : 'image.include-system-images') | translate }}</mat-slide-toggle>
</div>
<section fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap.gt-md="16px">
<span fxFlex></span>
@ -68,7 +68,7 @@
<button [disabled]="isLoading$ | async"
mat-icon-button
(click)="importImage()"
matTooltip="{{'image.import-image' | translate }}"
matTooltip="{{ (isScada ? 'scada.import-symbol' : 'image.import-image' ) | translate }}"
matTooltipPosition="above">
<tb-icon>mdi:file-import</tb-icon>
</button>
@ -77,7 +77,7 @@
mat-icon-button
color="primary"
(click)="uploadImage()"
matTooltip="{{'image.upload-image' | translate }}"
matTooltip="{{ (isScada ? 'scada.upload-symbol' : 'image.upload-image' ) | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
@ -87,18 +87,18 @@
mat-flat-button
color="primary"
(click)="uploadImage()">
{{ 'image.upload-image' | translate }}
{{ (isScada ? 'scada.upload-symbol' : 'image.upload-image' ) | translate }}
</button>
</section>
</div>
<mat-slide-toggle *ngIf="!isSysAdmin" fxHide fxShow.lt-lg
[ngModel]="includeSystemImages"
(ngModelChange)="includeSystemImagesChanged($event)">{{ 'image.include-system-images' | translate }}</mat-slide-toggle>
(ngModelChange)="includeSystemImagesChanged($event)">{{ (isScada ? 'scada.include-system-symbols' : 'image.include-system-images') | translate }}</mat-slide-toggle>
</mat-toolbar>
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="textSearchMode && (mode === 'grid' || dataSource?.selection.isEmpty())">
<div class="mat-toolbar-tools">
<button mat-icon-button
matTooltip="{{ 'image.search' | translate }}"
matTooltip="{{ (isScada ? 'scada.search' : 'image.search' ) | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
@ -106,7 +106,7 @@
<mat-label>&nbsp;</mat-label>
<input #searchInput matInput
[formControl]="textSearch"
placeholder="{{ 'image.search' | translate }}"/>
placeholder="{{ (isScada ? 'scada.search' : 'image.search' ) | translate }}"/>
</mat-form-field>
<button mat-icon-button (click)="exitFilterMode()"
matTooltip="{{ 'action.close' | translate }}"
@ -118,7 +118,7 @@
<mat-toolbar class="mat-mdc-table-toolbar" color="primary" [fxShow]="mode === 'list' && !dataSource?.selection.isEmpty()">
<div class="mat-toolbar-tools">
<span>
{{ translate.get('image.selected-images', {count: dataSource?.selection.selected.length}) | async }}
{{ translate.get((isScada ? 'scada.selected-symbols' : 'image.selected-images' ), {count: dataSource?.selection.selected.length}) | async }}
</span>
<span fxFlex></span>
<button mat-icon-button [disabled]="isLoading$ | async"
@ -187,36 +187,36 @@
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: '240px', maxWidth: '240px', width: '240px' }">
<mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: actionColumnWidth, maxWidth: actionColumnWidth, width: actionColumnWidth }">
</mat-header-cell>
<mat-cell *matCellDef="let image" [ngStyle.gt-md]="{ minWidth: '240px', maxWidth: '240px', width: '240px' }">
<mat-cell *matCellDef="let image" [ngStyle.gt-md]="{ minWidth: actionColumnWidth, maxWidth: actionColumnWidth, width: actionColumnWidth }">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'image.download-image' | translate }}"
matTooltip="{{ (isScada ? 'scada.download-symbol' : 'image.download-image' ) | translate }}"
matTooltipPosition="above"
(click)="downloadImage($event, image)">
<mat-icon>file_download</mat-icon>
</button>
<button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'image.export-image' | translate }}"
matTooltip="{{ (isScada ? 'scada.export-symbol' : 'image.export-image' ) | translate }}"
matTooltipPosition="above"
(click)="exportImage($event, image)">
<tb-icon>mdi:file-export</tb-icon>
</button>
<button mat-icon-button [disabled]="isLoading$ | async"
<button *ngIf="!isScada" mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'image.embed-image' | translate }}"
matTooltipPosition="above"
(click)="embedImage($event, image)">
<mat-icon>code</mat-icon>
</button>
<button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ (readonly(image) ? 'image.image-details' : 'image.edit-image') | translate }}"
matTooltip="{{ (readonly(image) ? (isScada ? 'scada.symbol-details' : 'image.image-details') : (isScada ? 'scada.edit-symbol' : 'image.edit-image')) | translate }}"
matTooltipPosition="above"
(click)="editImage($event, image)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button [disabled]="(isLoading$ | async) || !deleteEnabled(image)"
matTooltip="{{ 'image.delete-image' | translate }}"
matTooltip="{{ (isScada ? 'scada.delete-symbol' : 'image.delete-image' ) | translate }}"
matTooltipPosition="above"
(click)="deleteImage($event, image)">
<mat-icon>delete</mat-icon>
@ -233,15 +233,15 @@
[disabled]="isLoading$ | async"
(click)="downloadImage($event, image)">
<mat-icon>file_download</mat-icon>
<span translate>image.download-image</span>
<span>{{ (isScada ? 'scada.download-symbol' : 'image.download-image' ) | translate }}</span>
</button>
<button mat-menu-item
[disabled]="isLoading$ | async"
(click)="exportImage($event, image)">
<tb-icon matMenuItemIcon>mdi:file-export</tb-icon>
<span translate>image.export-image</span>
<span>{{ (isScada ? 'scada.export-symbol' : 'image.export-image' ) | translate }}</span>
</button>
<button mat-menu-item
<button *ngIf="!isScada" mat-menu-item
[disabled]="isLoading$ | async"
(click)="embedImage($event, image)">
<mat-icon>code</mat-icon>
@ -251,14 +251,14 @@
[disabled]="isLoading$ | async"
(click)="editImage($event, image)">
<mat-icon>edit</mat-icon>
<span>{{ (readonly(image) ? 'image.image-details' : 'image.edit-image') | translate }}</span>
<span>{{ (readonly(image) ? (isScada ? 'scada.symbol-details' : 'image.image-details') : (isScada ? 'scada.edit-symbol' : 'image.edit-image')) | translate }}</span>
</button>
<button mat-menu-item
[disabled]="isLoading$ | async"
[fxShow]="deleteEnabled(image)"
(click)="deleteImage($event, image)">
<mat-icon>delete</mat-icon>
<span translate>image.delete-image</span>
<span>{{ (isScada ? 'scada.delete-symbol' : 'image.delete-image' ) | translate }}</span>
</button>
</mat-menu>
</div>
@ -318,7 +318,7 @@
<ng-template #noImages>
<div class="tb-no-images">
<div class="tb-no-data-bg"></div>
<div class="tb-no-data-text" translate>image.no-images</div>
<div class="tb-no-data-text">{{ (isScada ? 'scada.no-symbols' : 'image.no-images') | translate }}</div>
</div>
</ng-template>
<ng-template #imageCard let-item="item" let-itemIndex="itemIndex">
@ -326,18 +326,18 @@
<div class="tb-image-card-overlay">
<div *ngIf="!selectionMode" class="tb-image-card-overlay-buttons">
<button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'image.download-image' | translate }}"
matTooltip="{{ (isScada ? 'scada.download-symbol' : 'image.download-image' ) | translate }}"
matTooltipPosition="above"
(click)="downloadImage($event, item)">
<mat-icon>file_download</mat-icon>
</button>
<button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'image.export-image' | translate }}"
matTooltip="{{ (isScada ? 'scada.export-symbol' : 'image.export-image' ) | translate }}"
matTooltipPosition="above"
(click)="exportImage($event, item)">
<tb-icon>mdi:file-export</tb-icon>
</button>
<button mat-icon-button [disabled]="isLoading$ | async"
<button *ngIf="!isScada" mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'image.embed-image' | translate }}"
matTooltipPosition="above"
(click)="embedImage($event, item, itemIndex)">
@ -345,7 +345,7 @@
</button>
<button *ngIf="deleteEnabled(item)"
mat-icon-button [disabled]="(isLoading$ | async)"
matTooltip="{{ 'image.delete-image' | translate }}"
matTooltip="{{ (isScada ? 'scada.delete-symbol' : 'image.delete-image' ) | translate }}"
matTooltipPosition="above"
(click)="deleteImage($event, item, itemIndex)">
<mat-icon>delete</mat-icon>
@ -359,7 +359,7 @@
mat-flat-button
color="primary"
(click)="editImage($event, item, itemIndex)">
{{ (readonly(item) ? 'image.image-details' : 'image.edit-image') | translate }}
{{ (readonly(item) ? (isScada ? 'scada.symbol-details' : 'image.image-details') : (isScada ? 'scada.edit-symbol' : 'image.edit-image')) | translate }}
</button>
<button
*ngIf="selectionMode"

71
ui-ngx/src/app/shared/components/image/image-gallery.component.ts

@ -18,6 +18,7 @@ import {
ImageResourceInfo,
ImageResourceInfoWithReferences,
imageResourceType,
ResourceSubType,
toImageDeleteResult
} from '@shared/models/resource.models';
import { forkJoin, merge, Observable, of, Subject, Subscription } from 'rxjs';
@ -29,10 +30,13 @@ import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef, EventEmitter, HostBinding,
ElementRef,
EventEmitter,
HostBinding,
Input,
OnDestroy,
OnInit, Output,
OnInit,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core';
@ -121,6 +125,9 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
@coerceBoolean()
dialogMode = false;
@Input()
imageSubType = ResourceSubType.IMAGE;
@Input()
mode: 'list' | 'grid' = 'list';
@ -168,6 +175,12 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
authUser = getCurrentAuthUser(this.store);
actionColumnWidth = '240px';
get isScada() {
return this.imageSubType === ResourceSubType.IOT_SVG;
}
private updateDataSubscription: Subscription;
private widgetResize$: ResizeObserver;
@ -192,7 +205,7 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
property: 'createdTime',
direction: Direction.DESC
});
return this.imageService.getImages(pageLink, filter.includeSystemImages);
return this.imageService.getImages(pageLink, this.imageSubType, filter.includeSystemImages);
};
}
@ -203,6 +216,7 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3];
const routerQueryParams: PageQueryParam = this.route.snapshot.queryParams;
if (this.pageMode) {
this.imageSubType = this.route.snapshot.data.imageSubType || ResourceSubType.IMAGE;
if (routerQueryParams.hasOwnProperty('direction')
|| routerQueryParams.hasOwnProperty('property')) {
sortOrder = {
@ -231,6 +245,9 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
this.dataSource = new ImagesDatasource(this.imageService, null,
entity => this.deleteEnabled(entity));
}
if (this.isScada) {
this.actionColumnWidth = '192px';
}
}
ngOnDestroy(): void {
@ -427,7 +444,7 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
} else {
this.pageLink.sortOrder = null;
}
this.dataSource.loadEntities(this.pageLink, this.includeSystemImages);
this.dataSource.loadEntities(this.pageLink, this.imageSubType, this.includeSystemImages);
} else {
this.gridComponent.update();
}
@ -482,8 +499,9 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
if ($event) {
$event.stopPropagation();
}
const title = this.translate.instant('image.delete-image-title', {imageTitle: image.title});
const content = this.translate.instant('image.delete-image-text');
const title = this.translate.instant(this.isScada ? 'scada.delete-symbol-title' : 'image.delete-image-title',
{imageTitle: image.title});
const content = this.translate.instant(this.isScada ? 'scada.delete-symbol-text' : 'image.delete-image-text');
this.dialogService.confirm(title, content,
this.translate.instant('action.no'),
this.translate.instant('action.yes')).subscribe((result) => {
@ -530,8 +548,9 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
}
const selectedImages = this.dataSource.selection.selected;
if (selectedImages && selectedImages.length) {
const title = this.translate.instant('image.delete-images-title', {count: selectedImages.length});
const content = this.translate.instant('image.delete-images-text');
const title = this.translate.instant(this.isScada ? 'scada.delete-symbols-title' : 'image.delete-images-title',
{count: selectedImages.length});
const content = this.translate.instant(this.isScada ? 'scada.delete-symbols-text' : 'image.delete-images-text');
this.dialogService.confirm(title, content,
this.translate.instant('action.no'),
this.translate.instant('action.yes')).subscribe((result) => {
@ -634,7 +653,9 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
ImageResourceInfo>(UploadImageDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {}
data: {
imageSubType: this.imageSubType
}
}).afterClosed().subscribe((result) => {
if (result) {
if (this.selectionMode) {
@ -650,19 +671,25 @@ export class ImageGalleryComponent extends PageComponent implements OnInit, OnDe
if ($event) {
$event.stopPropagation();
}
this.dialog.open<ImageDialogComponent, ImageDialogData,
ImageResourceInfo>(ImageDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
image,
readonly: this.readonly(image)
}
}).afterClosed().subscribe((result) => {
if (result) {
this.imageUpdated(result, itemIndex);
}
});
if (this.isScada) {
const type = imageResourceType(image);
const key = encodeURIComponent(image.resourceKey);
this.router.navigateByUrl(`resources/scada-symbols/${type}/${key}`);
} else {
this.dialog.open<ImageDialogComponent, ImageDialogData,
ImageResourceInfo>(ImageDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
image,
readonly: this.readonly(image)
}
}).afterClosed().subscribe((result) => {
if (result) {
this.imageUpdated(result, itemIndex);
}
});
}
}
embedImage($event: Event, image: ImageResourceInfo, itemIndex = -1) {

10
ui-ngx/src/app/shared/components/image/images-datasource.ts

@ -15,7 +15,7 @@
///
import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections';
import { ImageResourceInfo } from '@shared/models/resource.models';
import { ImageResourceInfo, ResourceSubType } from '@shared/models/resource.models';
import { BehaviorSubject, Observable, of, ReplaySubject, Subject } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { ImageService } from '@core/http/image.service';
@ -64,10 +64,10 @@ export class ImagesDatasource implements DataSource<ImageResourceInfo> {
}
}
loadEntities(pageLink: PageLink, includeSystemImages = false): Observable<PageData<ImageResourceInfo>> {
loadEntities(pageLink: PageLink, imageSubType: ResourceSubType, includeSystemImages = false): Observable<PageData<ImageResourceInfo>> {
this.dataLoading = true;
const result = new ReplaySubject<PageData<ImageResourceInfo>>();
this.fetchEntities(pageLink, includeSystemImages).pipe(
this.fetchEntities(pageLink, imageSubType, includeSystemImages).pipe(
tap(() => {
this.selection.clear();
}),
@ -83,8 +83,8 @@ export class ImagesDatasource implements DataSource<ImageResourceInfo> {
return result;
}
fetchEntities(pageLink: PageLink, includeSystemImages = false): Observable<PageData<ImageResourceInfo>> {
return this.imageService.getImages(pageLink, includeSystemImages);
fetchEntities(pageLink: PageLink, imageSubType: ResourceSubType, includeSystemImages = false): Observable<PageData<ImageResourceInfo>> {
return this.imageService.getImages(pageLink, imageSubType, includeSystemImages);
}
isAllSelected(): Observable<boolean> {

15
ui-ngx/src/app/shared/components/image/multiple-gallery-image-input.component.ts

@ -27,10 +27,14 @@ import { ImageLinkType } from '@shared/components/image/gallery-image-input.comp
import {
ImageResourceInfo,
prependTbImagePrefixToUrls,
removeTbImagePrefixFromUrls
removeTbImagePrefixFromUrls,
ResourceSubType
} from '@shared/models/resource.models';
import { MatDialog } from '@angular/material/dialog';
import { ImageGalleryDialogComponent } from '@shared/components/image/image-gallery-dialog.component';
import {
ImageGalleryDialogComponent,
ImageGalleryDialogData
} from '@shared/components/image/image-gallery-dialog.component';
@Component({
selector: 'tb-multiple-gallery-image-input',
@ -135,11 +139,14 @@ export class MultipleGalleryImageInputComponent extends PageComponent implements
if ($event) {
$event.stopPropagation();
}
this.dialog.open<ImageGalleryDialogComponent, any,
this.dialog.open<ImageGalleryDialogComponent, ImageGalleryDialogData,
ImageResourceInfo>(ImageGalleryDialogComponent, {
autoFocus: false,
disableClose: false,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
imageSubType: ResourceSubType.IMAGE
}
}).afterClosed().subscribe((image) => {
if (image) {
this.imageUrls.push(image.link);

6
ui-ngx/src/app/shared/components/image/upload-image-dialog.component.html

@ -17,7 +17,7 @@
-->
<form [formGroup]="uploadImageFormGroup" (ngSubmit)="upload()" style="width: 560px;">
<mat-toolbar color="primary">
<h2>{{ ( uploadImage ? 'image.upload-image' : 'image.update-image' ) | translate }}</h2>
<h2>{{ ( uploadImage ? (isScada ? 'scada.upload-symbol' : 'image.upload-image') : 'image.update-image' ) | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
@ -32,7 +32,9 @@
<fieldset [disabled]="isLoading$ | async">
<tb-image-input resultAsFile
processImageApiLink
label="{{'image.image-preview' | translate}}"
label="{{(isScada ? 'scada.symbol-preview' : 'image.image-preview') | translate}}"
[accept]="isScada ? 'image/svg+xml' : 'image/*'"
[allowedExtensions]="isScada ? 'svg' : null"
formControlName="file"
showFileName
[maxSizeByte]="maxResourceSize"

9
ui-ngx/src/app/shared/components/image/upload-image-dialog.component.ts

@ -30,12 +30,13 @@ import {
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { ImageService } from '@core/http/image.service';
import { ImageResource, ImageResourceInfo, imageResourceType } from '@shared/models/resource.models';
import { ImageResource, ImageResourceInfo, imageResourceType, ResourceSubType } from '@shared/models/resource.models';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { forkJoin } from 'rxjs';
import { blobToBase64 } from '@core/utils';
export interface UploadImageDialogData {
imageSubType: ResourceSubType;
image?: ImageResourceInfo;
}
@ -56,6 +57,10 @@ export class UploadImageDialogComponent extends
maxResourceSize = getCurrentAuthState(this.store).maxResourceSize;
get isScada() {
return this.data.imageSubType === ResourceSubType.IOT_SVG;
}
constructor(protected store: Store<AppState>,
protected router: Router,
private imageService: ImageService,
@ -101,7 +106,7 @@ export class UploadImageDialogComponent extends
if (this.uploadImage) {
const title: string = this.uploadImageFormGroup.get('title').value;
forkJoin([
this.imageService.uploadImage(file, title),
this.imageService.uploadImage(file, title, this.data.imageSubType),
blobToBase64(file)
]).subscribe(([imageInfo, base64]) => {
this.dialogRef.close(Object.assign(imageInfo, {base64}));

7
ui-ngx/src/app/shared/models/resource.models.ts

@ -27,6 +27,11 @@ export enum ResourceType {
JS_MODULE = 'JS_MODULE'
}
export enum ResourceSubType {
IMAGE = 'IMAGE',
IOT_SVG = 'IOT_SVG'
}
export const ResourceTypeMIMETypes = new Map<ResourceType, string>(
[
[ResourceType.LWM2M_MODEL, 'application/xml,text/xml'],
@ -59,6 +64,7 @@ export interface TbResourceInfo<D> extends Omit<BaseData<TbResourceId>, 'name' |
resourceKey?: string;
title?: string;
resourceType: ResourceType;
resourceSubType?: ResourceSubType;
fileName: string;
public: boolean;
publicResourceKey?: string;
@ -94,6 +100,7 @@ export interface ImageExportData {
mediaType: string;
fileName: string;
title: string;
subType: string;
resourceKey: string;
public: boolean;
publicResourceKey: string;

20
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -3392,6 +3392,26 @@
"iot-svg": {
"behavior": "Behavior"
},
"scada": {
"symbol": "SCADA symbol",
"symbols": "SCADA symbols",
"search": "Search symbol",
"selected-symbols": "{ count, plural, =1 {1 symbol} other {# symbols} } selected",
"download-symbol": "Download SCADA symbol",
"export-symbol": "Export SCADA symbol to JSON",
"import-symbol": "Import SCADA symbol from JSON",
"upload-symbol": "Upload SCADA symbol",
"edit-symbol": "Edit SCADA symbol",
"symbol-details": "SCADA symbol details",
"no-symbols": "No symbols found",
"delete-symbol": "Delete SCADA symbol",
"delete-symbol-title": "Are you sure you want to delete SCADA symbol '{{imageTitle}}'?",
"delete-symbol-text": "Be careful, after the confirmation SCADA symbol will become unrecoverable.",
"delete-symbols-title": "Are you sure you want to delete { count, plural, =1 {1 SCADA symbol} other {# SCADA symbols} }?",
"delete-symbols-text": "Be careful, after the confirmation all selected SCADA symbols will be removed and all related data will become unrecoverable.",
"include-system-symbols": "Include system symbols",
"symbol-preview": "Symbol preview"
},
"item": {
"selected": "Selected"
},

Loading…
Cancel
Save