,
+ @Inject(MAT_DIALOG_DATA) public data: ResourcesDialogData,
+ @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
+ private resourceService: ResourceService) {
+ super(store, router, dialogRef);
+
+ if (this.data.isAdd) {
+ this.isAdd = true;
+ }
+
+ if (this.data.resources) {
+ this.resources = this.data.resources;
+ }
+ }
+
+ ngAfterViewInit(): void {
+ if (this.isAdd) {
+ setTimeout(() => {
+ this.resourcesComponent.entityForm.markAsDirty();
+ }, 0);
+ }
+ }
+
+ isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
+ const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
+ const customErrorState = !!(control && control.invalid && this.submitted);
+ return originalErrorState || customErrorState;
+ }
+
+ cancel(): void {
+ this.dialogRef.close(null);
+ }
+
+ save(): void {
+ this.submitted = true;
+ if (this.resourcesComponent.entityForm.valid) {
+ const resource = {...this.resourcesComponent.entityFormValue()};
+ if (Array.isArray(resource.data)) {
+ const resources = [];
+ resource.data.forEach((data, index) => {
+ resources.push({
+ resourceType: resource.resourceType,
+ data,
+ fileName: resource.fileName[index],
+ title: resource.title
+ });
+ });
+ this.resourceService.saveResources(resources, {resendRequest: true}).pipe(
+ map((response) => response[0])
+ ).subscribe(result => this.dialogRef.close(result));
+ } else {
+ if (resource.resourceType !== ResourceType.GENERAL) {
+ delete resource.descriptor;
+ }
+ this.resourceService.saveResource(resource).subscribe(result => this.dialogRef.close(result));
+ }
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html
similarity index 56%
rename from ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html
rename to ui-ngx/src/app/modules/home/components/resources/resources-library.component.html
index ebb946ccbc..4737b75ef2 100644
--- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html
+++ b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html
@@ -15,7 +15,7 @@
limitations under the License.
-->
-
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts
index e4b21b3f18..dc50051a8a 100644
--- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts
+++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts
@@ -24,6 +24,8 @@ import { AiModel, AiRuleNodeResponseFormatTypeOnlyText, ResponseFormat } from '@
import { deepTrim } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
import { jsonRequired } from '@shared/components/json-object-edit.component';
+import { Resource, ResourceType } from "@shared/models/resource.models";
+import { ResourcesDialogComponent, ResourcesDialogData } from "@home/components/resources/resources-dialog.component";
@Component({
selector: 'tb-external-node-ai-config',
@@ -38,6 +40,9 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent {
responseFormat = ResponseFormat;
+ EntityType = EntityType;
+ ResourceType = ResourceType;
+
constructor(private fb: UntypedFormBuilder,
private translate: TranslateService,
private dialog: MatDialog) {
@@ -53,6 +58,7 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent {
modelId: [configuration?.modelId ?? null, [Validators.required]],
systemPrompt: [configuration?.systemPrompt ?? '', [Validators.maxLength(500_000), Validators.pattern(/.*\S.*/)]],
userPrompt: [configuration?.userPrompt ?? '', [Validators.required, Validators.maxLength(500_000), Validators.pattern(/.*\S.*/)]],
+ resourceIds: [configuration?.resourceIds ?? []],
responseFormat: this.fb.group({
type: [configuration?.responseFormat?.type ?? ResponseFormat.JSON, []],
schema: [configuration?.responseFormat?.schema ?? null, [jsonRequired]],
@@ -117,5 +123,23 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent {
this.aiConfigForm.get(formControl).markAsDirty();
}
});
+ };
+
+ createAiResources(name: string, formControl: string) {
+ this.dialog.open(ResourcesDialogComponent, {
+ disableClose: true,
+ panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
+ data: {
+ resources: {title: name, resourceType: ResourceType.GENERAL},
+ isAdd: true
+ }
+ }).afterClosed()
+ .subscribe((resource) => {
+ if (resource) {
+ const resourceIds = [...(this.aiConfigForm.get(formControl).value || []), resource.id.id];
+ this.aiConfigForm.get(formControl).patchValue(resourceIds);
+ this.aiConfigForm.get(formControl).markAsDirty();
+ }
+ });
}
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html
index dc0f467a16..31d0e6cd7e 100644
--- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html
@@ -144,6 +144,11 @@
+
+
+ {{ 'legend.show-total' | translate }}
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.ts
index 4a105c9cdd..7f86a5aebe 100644
--- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.ts
@@ -179,6 +179,7 @@ export abstract class LatestChartBasicConfigComponent
+
+
+ {{ 'tooltip.show-stack-total' | translate }}
+
+
+
+
+ {{ 'legend.show-total' | translate }}
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts
index fc5940406c..ff7641bf1a 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts
@@ -135,6 +135,7 @@ export abstract class LatestChartWidgetSettingsComponent
+
+
+ {{ 'tooltip.show-stack-total' | translate }}
+
+
diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts
index 552c4f1f71..9d1de180e9 100644
--- a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts
+++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts
@@ -14,7 +14,18 @@
/// limitations under the License.
///
-import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core';
+import {
+ Component,
+ ElementRef,
+ EventEmitter,
+ forwardRef,
+ Input,
+ OnChanges,
+ OnInit,
+ Output,
+ SimpleChanges,
+ ViewChild
+} from '@angular/core';
import {
ControlValueAccessor,
NG_VALIDATORS,
@@ -93,6 +104,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan
}
@Input()
+ @coerceBoolean()
disabled: boolean;
@Input()
@@ -109,6 +121,13 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan
@coerceBoolean()
inlineField: boolean;
+ @Input()
+ @coerceBoolean()
+ allowCreateNew: boolean;
+
+ @Output()
+ createNew = new EventEmitter();
+
@ViewChild('entityInput') entityInput: ElementRef;
@ViewChild('entityAutocomplete') matAutocomplete: MatAutocomplete;
@ViewChild('chipList', {static: true}) chipList: MatChipGrid;
@@ -136,6 +155,11 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan
this.entityListFormGroup.get('entities').updateValueAndValidity();
}
+ createNewEntity($event: Event, searchText?: string) {
+ $event.stopPropagation();
+ this.createNew.emit(searchText);
+ }
+
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
@@ -201,6 +225,9 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan
this.modelValue = null;
}
this.dirty = true;
+ if (this.entityInput) {
+ this.entityInput.nativeElement.value = '';
+ }
}
validate(): ValidationErrors | null {
diff --git a/ui-ngx/src/app/shared/components/file-input.component.ts b/ui-ngx/src/app/shared/components/file-input.component.ts
index bbe68bb9c6..6960db73dd 100644
--- a/ui-ngx/src/app/shared/components/file-input.component.ts
+++ b/ui-ngx/src/app/shared/components/file-input.component.ts
@@ -129,10 +129,15 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
@Output()
fileNameChanged = new EventEmitter();
+ @Output()
+ mediaTypeChanged = new EventEmitter();
+
fileName: string | string[];
fileContent: any;
files: File[];
+ mediaType: string;
+
@ViewChild('flow', {static: true})
flow: FlowDirective;
@@ -180,6 +185,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
this.fileContent = files[0].fileContent;
this.fileName = files[0].fileName;
this.files = files[0].files;
+ this.mediaType = files[0].mediaType;
this.updateModel();
} else if (files.length > 1) {
this.fileContent = files.map(content => content.fileContent);
@@ -203,6 +209,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
let fileName = null;
let fileContent = null;
let files = null;
+ let mediaType = null;
if (reader.readyState === reader.DONE) {
if (!this.workFromFileObj) {
fileContent = reader.result;
@@ -211,16 +218,18 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
fileContent = this.contentConvertFunction(fileContent);
}
fileName = fileContent ? file.name : null;
+ mediaType = file?.file?.type || null;
}
} else if (file.name || file.file){
files = file.file;
fileName = file.name;
+ mediaType = file.file.type || null;
}
}
- resolve({fileContent, fileName, files});
+ resolve({fileContent, fileName, files, mediaType});
};
reader.onerror = () => {
- resolve({fileContent: null, fileName: null, files: null});
+ resolve({fileContent: null, fileName: null, files: null, mediaType: null});
};
if (this.readAsBinary) {
reader.readAsBinaryString(file.file);
@@ -283,6 +292,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
this.propagateChange(this.files);
} else {
this.propagateChange(this.fileContent);
+ this.mediaTypeChanged.emit(this.mediaType);
this.fileNameChanged.emit(this.fileName);
}
}
diff --git a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts
index 42e04cfc46..af3d51de69 100644
--- a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts
+++ b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts
@@ -419,9 +419,10 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
const timewindowFormValue = this.timewindowForm.getRawValue();
const realtimeDisableCustomInterval = timewindowFormValue.realtime.disableCustomInterval;
const historyDisableCustomInterval = timewindowFormValue.history.disableCustomInterval;
- updateFormValuesOnTimewindowTypeChange(selectedTab, this.quickIntervalOnly, this.timewindowForm,
+ updateFormValuesOnTimewindowTypeChange(selectedTab, this.timewindowForm,
realtimeDisableCustomInterval, historyDisableCustomInterval,
- timewindowFormValue.realtime.advancedParams, timewindowFormValue.history.advancedParams);
+ timewindowFormValue.realtime.advancedParams, timewindowFormValue.history.advancedParams,
+ this.realtimeTimewindowOptions, this.historyTimewindowOptions);
this.timewindowForm.patchValue({
hideAggregation: timewindowFormValue.hideAggregation,
hideAggInterval: timewindowFormValue.hideAggInterval,
diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts
index 2f0a1aa603..c306ce4a49 100644
--- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts
+++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts
@@ -401,9 +401,10 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
}
private onTimewindowTypeChange(selectedTab: TimewindowType) {
- updateFormValuesOnTimewindowTypeChange(selectedTab, this.quickIntervalOnly, this.timewindowForm,
+ updateFormValuesOnTimewindowTypeChange(selectedTab, this.timewindowForm,
this.realtimeDisableCustomInterval, this.historyDisableCustomInterval,
- this.realtimeAdvancedParams, this.historyAdvancedParams);
+ this.realtimeAdvancedParams, this.historyAdvancedParams,
+ this.realtimeTimewindowOptions, this.historyTimewindowOptions);
}
update() {
diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts
index 94960cb838..34e584a527 100644
--- a/ui-ngx/src/app/shared/import-export/import-export.service.ts
+++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts
@@ -22,7 +22,6 @@ import { AppState } from '@core/core.state';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { BreakpointId, Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models';
import { deepClone, guid, isDefined, isNotEmptyStr, isObject, isString, isUndefined } from '@core/utils';
-import { WINDOW } from '@core/services/window.service';
import { DOCUMENT } from '@angular/common';
import {
AliasesInfo,
@@ -100,8 +99,7 @@ type SupportEntityResources = 'includeResourcesInExportWidgetTypes' | 'includeRe
@Injectable()
export class ImportExportService {
- constructor(@Inject(WINDOW) private window: Window,
- @Inject(DOCUMENT) private document: Document,
+ constructor(@Inject(DOCUMENT) private document: Document,
private store: Store,
private translate: TranslateService,
private dashboardService: DashboardService,
@@ -177,9 +175,7 @@ export class ImportExportService {
public exportCalculatedField(calculatedFieldId: string): void {
this.calculatedFieldsService.getCalculatedFieldById(calculatedFieldId).subscribe({
next: (calculatedField) => {
- let name = calculatedField.name;
- name = name.toLowerCase().replace(/\W/g, '_');
- this.exportToPc(this.prepareCalculatedFieldExport(calculatedField), name);
+ this.exportToPc(this.prepareCalculatedFieldExport(calculatedField), calculatedField.name, true);
},
error: (e) => {
this.handleExportError(e, 'calculated-fields.export-failed-error');
@@ -200,9 +196,7 @@ export class ImportExportService {
this.updateUserSettingsIncludeResourcesIfNeeded(includeResources, result.include, 'includeResourcesInExportDashboard');
this.dashboardService.exportDashboard(dashboardId, result.include).subscribe({
next: (dashboard) => {
- let name = dashboard.title;
- name = name.toLowerCase().replace(/\W/g, '_');
- this.exportToPc(this.prepareDashboardExport(dashboard), name);
+ this.exportToPc(this.prepareDashboardExport(dashboard), dashboard.title, true);
},
error: (e) => {
this.handleExportError(e, 'dashboard.export-failed-error');
@@ -261,9 +255,8 @@ export class ImportExportService {
widgetTitle: string, breakpoint: BreakpointId) {
const widgetItem = this.itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget, breakpoint);
const widgetDefaultName = this.widgetService.getWidgetInfoFromCache(widget.typeFullFqn).widgetName;
- let fileName = widgetDefaultName + (isNotEmptyStr(widgetTitle) ? `_${widgetTitle}` : '');
- fileName = fileName.toLowerCase().replace(/\W/g, '_');
- this.exportToPc(this.prepareExport(widgetItem), fileName);
+ const fileName = widgetDefaultName + (isNotEmptyStr(widgetTitle) ? `_${widgetTitle}` : '');
+ this.exportToPc(this.prepareExport(widgetItem), fileName, true);
}
public importWidget(dashboard: Dashboard, targetState: string,
@@ -360,9 +353,7 @@ export class ImportExportService {
this.updateUserSettingsIncludeResourcesIfNeeded(includeResources, result.include, 'includeResourcesInExportWidgetTypes');
this.widgetService.exportWidgetType(widgetTypeId, result.include).subscribe({
next: (widgetTypeDetails) => {
- let name = widgetTypeDetails.name;
- name = name.toLowerCase().replace(/\W/g, '_');
- this.exportToPc(this.prepareExport(widgetTypeDetails), name);
+ this.exportToPc(this.prepareExport(widgetTypeDetails), widgetTypeDetails.name, true);
},
error: (e) => {
this.handleExportError(e, 'widget-type.export-failed-error');
@@ -440,7 +431,7 @@ export class ImportExportService {
public exportEntity(entityData: VersionedEntity): void {
const id = (entityData as EntityInfoData).id ?? (entityData as RuleChainMetaData).ruleChainId;
let fileName = (entityData as EntityInfoData).name;
- let preparedData;
+ let preparedData: any;
switch (id.entityType) {
case EntityType.DEVICE_PROFILE:
case EntityType.ASSET_PROFILE:
@@ -511,9 +502,7 @@ export class ImportExportService {
for (const widgetTypeDetails of widgetTypesDetails) {
widgetsBundleItem.widgetTypes.push(this.prepareExport(widgetTypeDetails));
}
- let name = widgetsBundle.title;
- name = name.toLowerCase().replace(/\W/g, '_');
- this.exportToPc(widgetsBundleItem, name);
+ this.exportToPc(widgetsBundleItem, widgetsBundle.title, true);
},
error: (e) => {
this.handleExportError(e, 'widgets-bundle.export-failed-error');
@@ -528,9 +517,7 @@ export class ImportExportService {
widgetsBundle: this.prepareExport(widgetsBundle),
widgetTypeFqns
};
- let name = widgetsBundle.title;
- name = name.toLowerCase().replace(/\W/g, '_');
- this.exportToPc(widgetsBundleItem, name);
+ this.exportToPc(widgetsBundleItem, widgetsBundle.title, true);
},
error: (e) => {
this.handleExportError(e, 'widgets-bundle.export-failed-error');
@@ -662,11 +649,9 @@ export class ImportExportService {
private onRuleChainExported() {
return {
next: (ruleChainExport: RuleChainImport) => {
- let name = ruleChainExport.ruleChain.name;
- name = name.toLowerCase().replace(/\W/g, '_');
- this.exportToPc(ruleChainExport, name);
+ this.exportToPc(ruleChainExport, ruleChainExport.ruleChain.name, true);
},
- error: (e) => {
+ error: (e: any) => {
this.handleExportError(e, 'rulechain.export-failed-error');
}
};
@@ -747,9 +732,7 @@ export class ImportExportService {
public exportDeviceProfile(deviceProfileId: string) {
this.deviceProfileService.exportDeviceProfile(deviceProfileId).subscribe({
next: (deviceProfile) => {
- let name = deviceProfile.name;
- name = name.toLowerCase().replace(/\W/g, '_');
- this.exportToPc(this.prepareProfileExport(deviceProfile), name);
+ this.exportToPc(this.prepareProfileExport(deviceProfile), deviceProfile.name, true);
},
error: (e) => {
this.handleExportError(e, 'device-profile.export-failed-error');
@@ -776,9 +759,7 @@ export class ImportExportService {
public exportAssetProfile(assetProfileId: string) {
this.assetProfileService.exportAssetProfile(assetProfileId).subscribe({
next: (assetProfile) => {
- let name = assetProfile.name;
- name = name.toLowerCase().replace(/\W/g, '_');
- this.exportToPc(this.prepareProfileExport(assetProfile), name);
+ this.exportToPc(this.prepareProfileExport(assetProfile), assetProfile.name, true);
},
error: (e) => {
this.handleExportError(e, 'asset-profile.export-failed-error');
@@ -805,9 +786,7 @@ export class ImportExportService {
public exportTenantProfile(tenantProfileId: string) {
this.tenantProfileService.getTenantProfile(tenantProfileId).subscribe({
next: (tenantProfile) => {
- let name = tenantProfile.name;
- name = name.toLowerCase().replace(/\W/g, '_');
- this.exportToPc(this.prepareProfileExport(tenantProfile), name);
+ this.exportToPc(this.prepareProfileExport(tenantProfile), tenantProfile.name, true);
},
error: (e) => {
this.handleExportError(e, 'tenant-profile.export-failed-error');
@@ -842,7 +821,7 @@ export class ImportExportService {
return cellData;
}
- public exportCsv(data: {[key: string]: any}[], filename: string) {
+ public exportCsv(data: {[key: string]: any}[], filename: string, normalizeFileName = false) {
let colsHead: string;
let colsData: string;
if (data && data.length) {
@@ -857,18 +836,18 @@ export class ImportExportService {
colsData = '';
}
const csvData = `${colsHead}\n${colsData}`;
- this.downloadFile(csvData, filename, CSV_TYPE);
+ this.downloadFile(csvData, filename, CSV_TYPE, normalizeFileName);
}
- public exportText(data: string | Array, filename: string) {
+ public exportText(data: string | Array, filename: string, normalizeFileName = false) {
let content = data;
if (Array.isArray(data)) {
content = data.join('\n');
}
- this.downloadFile(content, filename, TEXT_TYPE);
+ this.downloadFile(content, filename, TEXT_TYPE, normalizeFileName);
}
- public exportJSZip(data: object, filename: string): Observable {
+ public exportJSZip(data: object, filename: string, normalizeFileName = false): Observable {
const exportJsSubjectSubject = new Subject();
import('jszip').then((JSZip) => {
try {
@@ -880,9 +859,9 @@ export class ImportExportService {
}
}
jsZip.generateAsync({type: 'blob'}).then(content => {
- this.downloadFile(content, filename, ZIP_TYPE);
+ this.downloadFile(content, filename, ZIP_TYPE, normalizeFileName);
exportJsSubjectSubject.next(null);
- }).catch(e => {
+ }).catch((e: any) => {
exportJsSubjectSubject.error(e);
});
} catch (e) {
@@ -1180,42 +1159,40 @@ export class ImportExportService {
));
}
- private exportToPc(data: any, filename: string) {
+ private exportToPc(data: any, filename: string, normalizeFileName = false) {
if (!data) {
console.error('No data');
return;
}
- this.exportJson(data, filename);
+ this.exportJson(data, filename, normalizeFileName);
}
- public exportJson(data: any, filename: string) {
+ public exportJson(data: any, filename: string, normalizeFileName = false) {
if (isObject(data)) {
data = JSON.stringify(data, null, 2);
}
- this.downloadFile(data, filename, JSON_TYPE);
+ this.downloadFile(data, filename, JSON_TYPE, normalizeFileName);
}
- private downloadFile(data: any, filename: string, fileType: FileType) {
- if (!filename) {
- filename = 'download';
+ private prepareFilename(filename: string, extension: string, normalizeFileName = false): string {
+ if (normalizeFileName) {
+ filename = filename.toLowerCase().replace(/\s/g, '_');
}
- filename += '.' + fileType.extension;
+ filename = filename.replace(/[\\/<>:"|?*\s]/g, '_');
+ return `${filename}.${extension}`;
+ }
+
+ private downloadFile(data: any, filename = 'download', fileType: FileType, normalizeFileName: boolean) {
+ filename = this.prepareFilename(filename, fileType.extension, normalizeFileName);
const blob = new Blob([data], {type: fileType.mimeType});
- // @ts-ignore
- if (this.window.navigator && this.window.navigator.msSaveOrOpenBlob) {
- // @ts-ignore
- this.window.navigator.msSaveOrOpenBlob(blob, filename);
- } else {
- const e = this.document.createEvent('MouseEvents');
- const a = this.document.createElement('a');
- a.download = filename;
- a.href = URL.createObjectURL(blob);
- a.dataset.downloadurl = [fileType.mimeType, a.download, a.href].join(':');
- // @ts-ignore
- e.initEvent('click', true, false, this.window,
- 0, 0, 0, 0, 0, false, false, false, false, 0, null);
- a.dispatchEvent(e);
- }
+ const url = URL.createObjectURL(blob);
+
+ const a = this.document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ a.dataset.downloadurl = [fileType.mimeType, a.download, a.href].join(':');
+ a.click();
+ setTimeout(() => URL.revokeObjectURL(url), 0);
}
private prepareDashboardExport(dashboard: Dashboard): Dashboard {
diff --git a/ui-ngx/src/app/shared/models/ai-model.models.ts b/ui-ngx/src/app/shared/models/ai-model.models.ts
index f3161263b7..d3b70f9a74 100644
--- a/ui-ngx/src/app/shared/models/ai-model.models.ts
+++ b/ui-ngx/src/app/shared/models/ai-model.models.ts
@@ -34,6 +34,13 @@ export interface AiModel extends Omit, 'label'>, HasTenantId
region?: string;
accessKeyId?: string;
secretAccessKey?: string;
+ baseUrl?: string;
+ auth?: {
+ type: AuthenticationType;
+ username?: string;
+ password?: string;
+ token?: string
+ }
};
modelId: string;
temperature?: number;
@@ -42,6 +49,7 @@ export interface AiModel extends Omit, 'label'>, HasTenantId
frequencyPenalty?: number;
presencePenalty?: number;
maxOutputTokens?: number;
+ contextLength?: number;
}
}
@@ -57,7 +65,8 @@ export enum AiProvider {
MISTRAL_AI = 'MISTRAL_AI',
ANTHROPIC = 'ANTHROPIC',
AMAZON_BEDROCK = 'AMAZON_BEDROCK',
- GITHUB_MODELS = 'GITHUB_MODELS'
+ GITHUB_MODELS = 'GITHUB_MODELS',
+ OLLAMA = 'OLLAMA'
}
export const AiProviderTranslations = new Map(
@@ -69,7 +78,8 @@ export const AiProviderTranslations = new Map(
[AiProvider.MISTRAL_AI , 'ai-models.ai-providers.mistral-ai'],
[AiProvider.ANTHROPIC , 'ai-models.ai-providers.anthropic'],
[AiProvider.AMAZON_BEDROCK , 'ai-models.ai-providers.amazon-bedrock'],
- [AiProvider.GITHUB_MODELS , 'ai-models.ai-providers.github-models']
+ [AiProvider.GITHUB_MODELS , 'ai-models.ai-providers.github-models'],
+ [AiProvider.OLLAMA , 'ai-models.ai-providers.ollama']
]
);
@@ -84,10 +94,11 @@ export const ProviderFieldsAllList = [
'serviceVersion',
'region',
'accessKeyId',
- 'secretAccessKey'
+ 'secretAccessKey',
+ 'baseUrl'
];
-export const ModelFieldsAllList = ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'];
+export const ModelFieldsAllList = ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens', 'contextLength'];
export const AiModelMap = new Map([
[
@@ -99,13 +110,16 @@ export const AiModelMap = new Map(
[ResourceType.LWM2M_MODEL, 'resource.type.lwm2m-model'],
[ResourceType.PKCS_12, 'resource.type.pkcs-12'],
[ResourceType.JKS, 'resource.type.jks'],
- [ResourceType.JS_MODULE, 'resource.type.js-module']
+ [ResourceType.JS_MODULE, 'resource.type.js-module'],
+ [ResourceType.GENERAL, 'resource.type.general'],
]
);
@@ -76,8 +78,8 @@ export interface TbResourceInfo extends Omit, 'name' |
title?: string;
resourceType: ResourceType;
resourceSubType?: ResourceSubType;
- fileName: string;
- public: boolean;
+ fileName?: string;
+ public?: boolean;
publicResourceKey?: string;
readonly link?: string;
readonly publicLink?: string;
@@ -87,7 +89,7 @@ export interface TbResourceInfo extends Omit, 'name' |
export type ResourceInfo = TbResourceInfo;
export interface Resource extends ResourceInfo {
- data: string;
+ data?: string;
name?: string;
}
diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts
index 03ee9e626a..f84a883235 100644
--- a/ui-ngx/src/app/shared/models/time/time.models.ts
+++ b/ui-ngx/src/app/shared/models/time/time.models.ts
@@ -20,6 +20,7 @@ import moment_ from 'moment';
import * as momentTz from 'moment-timezone';
import { IntervalType } from '@shared/models/telemetry/telemetry.models';
import { FormGroup } from '@angular/forms';
+import { ToggleHeaderOption } from '@shared/components/toggle-header.component';
const moment = moment_;
@@ -524,17 +525,20 @@ export const timewindowTypeChanged = (newTimewindow: Timewindow, oldTimewindow:
};
export const updateFormValuesOnTimewindowTypeChange = (selectedTab: TimewindowType,
- quickIntervalOnly: boolean, timewindowForm: FormGroup,
+ timewindowForm: FormGroup,
realtimeDisableCustomInterval: boolean, historyDisableCustomInterval: boolean,
- realtimeAdvancedParams?: TimewindowAdvancedParams,
- historyAdvancedParams?: TimewindowAdvancedParams) => {
+ realtimeAdvancedParams: TimewindowAdvancedParams,
+ historyAdvancedParams: TimewindowAdvancedParams,
+ realtimeTimewindowOptions: ToggleHeaderOption[],
+ historyTimewindowOptions: ToggleHeaderOption[]) => {
const timewindowFormValue = timewindowForm.getRawValue();
if (selectedTab === TimewindowType.REALTIME) {
- if (timewindowFormValue.history.historyType !== HistoryWindowType.FIXED
- && !(quickIntervalOnly && timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL)) {
- if (Object.keys(RealtimeWindowType).includes(HistoryWindowType[timewindowFormValue.history.historyType])) {
- timewindowForm.get('realtime.realtimeType').patchValue(RealtimeWindowType[HistoryWindowType[timewindowFormValue.history.historyType]]);
- }
+ const sameWindowTypeOptionAvailable = realtimeTimewindowOptions.some(
+ option => {
+ return option.value === RealtimeWindowType[HistoryWindowType[timewindowFormValue.history.historyType]]
+ });
+ if (sameWindowTypeOptionAvailable) {
+ timewindowForm.get('realtime.realtimeType').patchValue(RealtimeWindowType[HistoryWindowType[timewindowFormValue.history.historyType]]);
if (!realtimeDisableCustomInterval ||
!realtimeAdvancedParams?.allowedLastIntervals?.length || realtimeAdvancedParams.allowedLastIntervals.includes(timewindowFormValue.history.timewindowMs)) {
timewindowForm.get('realtime.timewindowMs').patchValue(timewindowFormValue.history.timewindowMs);
@@ -552,20 +556,26 @@ export const updateFormValuesOnTimewindowTypeChange = (selectedTab: TimewindowTy
}
}
} else {
- timewindowForm.get('history.historyType').patchValue(HistoryWindowType[RealtimeWindowType[timewindowFormValue.realtime.realtimeType]]);
- if (!historyDisableCustomInterval ||
+ const sameWindowTypeOptionAvailable = historyTimewindowOptions.some(
+ option => {
+ return option.value === HistoryWindowType[RealtimeWindowType[timewindowFormValue.realtime.realtimeType]]
+ });
+ if (sameWindowTypeOptionAvailable) {
+ timewindowForm.get('history.historyType').patchValue(HistoryWindowType[RealtimeWindowType[timewindowFormValue.realtime.realtimeType]]);
+ if (!historyDisableCustomInterval ||
!historyAdvancedParams?.allowedLastIntervals?.length || historyAdvancedParams.allowedLastIntervals?.includes(timewindowFormValue.realtime.timewindowMs)) {
- timewindowForm.get('history.timewindowMs').patchValue(timewindowFormValue.realtime.timewindowMs);
- }
- if (!historyAdvancedParams?.allowedQuickIntervals?.length || historyAdvancedParams.allowedQuickIntervals?.includes(timewindowFormValue.realtime.quickInterval)) {
- timewindowForm.get('history.quickInterval').patchValue(timewindowFormValue.realtime.quickInterval);
- }
- const defaultAggInterval = historyDefaultAggInterval(timewindowForm.getRawValue(), historyAdvancedParams);
- const allowedAggIntervals = historyAllowedAggIntervals(timewindowForm.getRawValue(), historyAdvancedParams);
- if (defaultAggInterval || !allowedAggIntervals.length || allowedAggIntervals.includes(timewindowFormValue.realtime.interval)) {
- setTimeout(() => timewindowForm.get('history.interval').patchValue(
- defaultAggInterval ?? timewindowFormValue.realtime.interval
- ));
+ timewindowForm.get('history.timewindowMs').patchValue(timewindowFormValue.realtime.timewindowMs);
+ }
+ if (!historyAdvancedParams?.allowedQuickIntervals?.length || historyAdvancedParams.allowedQuickIntervals?.includes(timewindowFormValue.realtime.quickInterval)) {
+ timewindowForm.get('history.quickInterval').patchValue(timewindowFormValue.realtime.quickInterval);
+ }
+ const defaultAggInterval = historyDefaultAggInterval(timewindowForm.getRawValue(), historyAdvancedParams);
+ const allowedAggIntervals = historyAllowedAggIntervals(timewindowForm.getRawValue(), historyAdvancedParams);
+ if (defaultAggInterval || !allowedAggIntervals.length || allowedAggIntervals.includes(timewindowFormValue.realtime.interval)) {
+ setTimeout(() => timewindowForm.get('history.interval').patchValue(
+ defaultAggInterval ?? timewindowFormValue.realtime.interval
+ ));
+ }
}
}
timewindowForm.patchValue({
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json
index 1866ab7d0b..f78e95e56d 100644
--- a/ui-ngx/src/assets/locale/locale.constant-en_US.json
+++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json
@@ -1194,13 +1194,15 @@
"mistral-ai": "Mistral AI",
"anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock",
- "github-models": "GitHub Models"
+ "github-models": "GitHub Models",
+ "ollama": "Ollama"
},
"name-required": "Name is required.",
"name-max-length": "Name must be 255 characters or less.",
"provider": "Provider",
"api-key": "API key",
"api-key-required": "API key is required.",
+ "api-key-open-ai-required": "API key is required when using the official OpenAI API.",
"project-id": "Project ID",
"project-id-required": "Project ID is required",
"location": "Location",
@@ -1237,17 +1239,34 @@
"frequency-penalty": "Frequency penalty",
"frequency-penalty-hint": "Applies a penalty to a token's likelihood that increases based on its frequency in the text.",
"max-output-tokens": "Maximum output tokens",
- "max-output-tokens-min": "Must be greater than 0.",
"max-output-tokens-hint": "Sets the maximum number of tokens that the \nmodel can generate in a single response.",
+ "context-length": "Context length",
+ "context-length-hint": "Defines the size of the context window in tokens. This value sets the total memory limit for the model, including both the user's input and the generated response.",
"endpoint": "Endpoint",
"endpoint-required": "Endpoint is required.",
+ "baseurl": "Base URL",
+ "baseurl-required": "Base URL is required.",
"service-version": "Service version",
"check-connectivity": "Check connectivity",
"check-connectivity-success": "Test request was successful",
"check-connectivity-failed": "Test request failed",
"no-model-matching": "No models matching '{{entity}}' were found.",
"model-required": "Model is required.",
- "no-model-text": "No models found."
+ "no-model-text": "No models found.",
+ "authentication": "Authentication",
+ "authentication-basic-hint": "Uses standard HTTP Basic authentication. The username and password will be combined, Base64-encoded, and sent in an \"Authorization\" header with each request to the Ollama server.",
+ "authentication-token-hint": "Uses Bearer token authentication. The provided token will be sent directly in an \"Authorization\" eader with each request to the Ollama server.",
+ "authentication-type": {
+ "none": "None",
+ "basic": "Basic",
+ "token": "Token"
+ },
+ "username": "Username",
+ "username-required": "Username is required.",
+ "password": "Password",
+ "password-required": "Password is required.",
+ "token": "Token",
+ "token-required": "Token is required."
},
"confirm-on-exit": {
"message": "You have unsaved changes. Are you sure you want to leave this page?",
@@ -4572,7 +4591,8 @@
"jks": "JKS",
"js-module": "JS module",
"lwm2m-model": "LWM2M model",
- "pkcs-12": "PKCS #12"
+ "pkcs-12": "PKCS #12",
+ "general": "General"
},
"resource-sub-type": "Sub-type",
"sub-type": {
@@ -4580,7 +4600,12 @@
"scada-symbol": "Scada symbol",
"extension": "Extension",
"module": "Module"
- }
+ },
+ "resource-is-in-use": "Resource is used by other entities",
+ "resources-are-in-use": "Resources are used by other entities",
+ "resource-is-in-use-text": "The Resource '{{title}}' was not deleted because it is used by the following entities:",
+ "resources-are-in-use-text": "Not all Resources have been deleted because they are used by other entities.You can view referenced entities by clicking the References button in the corresponding resource row.If you still want to delete these resources, select them in the table below and click the Delete selected button.",
+ "delete-resource-in-use-text": "If you still want to delete the resource, click the Delete anyway button."
},
"javascript": {
"add": "Add JavaScript resource",
@@ -5551,7 +5576,8 @@
"timeout-required": "Timeout is required",
"timeout-validation": "Must be from 1 second to 10 minutes.",
"force-acknowledgement": "Force acknowledgement",
- "force-acknowledgement-hint": "If enabled, the incoming message is acknowledged immediately. The model's response is then enqueued as a separate, new message."
+ "force-acknowledgement-hint": "If enabled, the incoming message is acknowledged immediately. The model's response is then enqueued as a separate, new message.",
+ "ai-resources": "AI resources"
}
},
"timezone": {
@@ -6090,6 +6116,7 @@
"show-date-time-interval": "Show date time interval",
"show-date-time-interval-hint": "Show date time interval according to the data aggregation.",
"hide-zero-tooltip-values": "Hide zero values",
+ "show-stack-total": "Show total value in stack mode",
"background-color": "Background color",
"background-blur": "Background blur"
},