UI: Prevent deleting an entity alias/filter that’s still used in map widgets

This commit is contained in:
Vladyslav_Prykhodko 2025-06-09 15:41:27 +03:00
parent 18b6a9557d
commit 429f9c7cc9
6 changed files with 74 additions and 33 deletions

View File

@ -31,7 +31,8 @@ import {
DashboardLayoutsInfo,
DashboardState,
DashboardStateLayouts,
GridSettings, LayoutType,
GridSettings,
LayoutType,
WidgetLayout
} from '@shared/models/dashboard.models';
import { deepClone, isDefined, isDefinedAndNotNull, isNotEmptyStr, isString, isUndefined } from '@core/utils';
@ -61,6 +62,7 @@ import { MediaBreakpoints } from '@shared/models/constants';
import { TranslateService } from '@ngx-translate/core';
import { DashboardPageLayout } from '@home/components/dashboard-page/dashboard-page.models';
import { maxGridsterCol, maxGridsterRow } from '@home/models/dashboard-component.models';
import { findWidgetModelDefinition } from '@shared/models/widget/widget-model.definition';
@Injectable({
providedIn: 'root'
@ -398,6 +400,14 @@ export class DashboardUtilsService {
return datasources;
}
public getWidgetDatasources(widget: Widget): Datasource[] {
const widgetDefinition = findWidgetModelDefinition(widget);
if (widgetDefinition) {
return widgetDefinition.datasources(widget);
}
return this.validateAndUpdateDatasources(widget.config.datasources);
}
public createDefaultLayoutData(): DashboardLayout {
return {
widgets: {},

View File

@ -35,7 +35,7 @@ import { FcRuleNode, ruleNodeTypeDescriptors } from '@shared/models/rule-node.mo
import { RuleChainService } from '@core/http/rule-chain.service';
import { RuleChainImport } from '@shared/models/rule-chain.models';
import { Filter, FilterInfo, Filters, FiltersInfo, getFilterId } from '@shared/models/query/query.models';
import { getWidgetExportDefinition } from '@shared/models/widget/widget-export.models';
import { findWidgetModelDefinition } from '@shared/models/widget/widget-model.definition';
const WIDGET_ITEM = 'widget_item';
const WIDGET_REFERENCE = 'widget_reference';
@ -142,7 +142,7 @@ export class ItemBufferService {
}
}
let widgetExportInfo: any;
const exportDefinition = getWidgetExportDefinition(widget);
const exportDefinition = findWidgetModelDefinition(widget);
if (exportDefinition) {
widgetExportInfo = exportDefinition.prepareExportInfo(dashboard, widget);
}
@ -270,7 +270,7 @@ export class ItemBufferService {
let callFilterUpdateFunction = false;
let newEntityAliases: EntityAliases;
let newFilters: Filters;
const exportDefinition = getWidgetExportDefinition(widget);
const exportDefinition = findWidgetModelDefinition(widget);
if (exportDefinition && widgetExportInfo || aliasesInfo) {
newEntityAliases = deepClone(dashboard.configuration.entityAliases);
}

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, DestroyRef, Inject, OnInit, SkipSelf } from '@angular/core';
import { Component, DestroyRef, Inject, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
@ -60,7 +60,7 @@ export interface EntityAliasesDialogData {
styleUrls: ['./entity-aliases-dialog.component.scss']
})
export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesDialogComponent, EntityAliases>
implements OnInit, ErrorStateMatcher {
implements ErrorStateMatcher {
title: string;
disableAdd: boolean;
@ -107,8 +107,7 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
this.addWidgetTitleToWidgetsMap(widget.config.alarmSource.entityAliasId, widget.config.title);
}
} else {
const datasources = this.dashboardUtils.validateAndUpdateDatasources(widget.config.datasources);
datasources.forEach((datasource) => {
this.dashboardUtils.getWidgetDatasources(widget).forEach((datasource) => {
if ([DatasourceType.entity, DatasourceType.entityCount, DatasourceType.alarmCount].includes(datasource.type)
&& datasource.entityAliasId) {
this.addWidgetTitleToWidgetsMap(datasource.entityAliasId, widget.config.title);
@ -143,7 +142,9 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
widgetsTitleList = [];
this.aliasToWidgetsMap[aliasId] = widgetsTitleList;
}
widgetsTitleList.push(widgetTitle);
if (!widgetsTitleList.includes(widgetTitle)) {
widgetsTitleList.push(widgetTitle);
}
}
private createEntityAliasFormControl(aliasId: string, entityAlias: EntityAlias): AbstractControl {
@ -166,9 +167,6 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
return this.entityAliasesFormGroup.get('entityAliases') as UntypedFormArray;
}
ngOnInit(): void {
}
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);

View File

@ -14,19 +14,19 @@
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { Component, Inject, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
AbstractControl,
FormGroupDirective,
NgForm,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
FormGroupDirective,
NgForm,
Validators
} from '@angular/forms';
import { Router } from '@angular/router';
@ -58,7 +58,7 @@ export interface FiltersDialogData {
styleUrls: ['./filters-dialog.component.scss']
})
export class FiltersDialogComponent extends DialogComponent<FiltersDialogComponent, Filters>
implements OnInit, ErrorStateMatcher {
implements ErrorStateMatcher {
title: string;
disableAdd: boolean;
@ -96,15 +96,16 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
}
} else {
this.data.widgets.forEach((widget) => {
const datasources = this.dashboardUtils.validateAndUpdateDatasources(widget.config.datasources);
datasources.forEach((datasource) => {
if (datasource.type === DatasourceType.entity && datasource.filterId) {
this.dashboardUtils.getWidgetDatasources(widget).forEach((datasource) => {
if (datasource.type !== DatasourceType.function && datasource.filterId) {
widgetsTitleList = this.filterToWidgetsMap[datasource.filterId];
if (!widgetsTitleList) {
widgetsTitleList = [];
this.filterToWidgetsMap[datasource.filterId] = widgetsTitleList;
}
widgetsTitleList.push(widget.config.title);
if (!widgetsTitleList.includes(widget.config.title)) {
widgetsTitleList.push(widget.config.title);
}
}
});
});
@ -140,9 +141,6 @@ export class FiltersDialogComponent extends DialogComponent<FiltersDialogCompone
return this.filtersFormGroup.get('filters') as UntypedFormArray;
}
ngOnInit(): void {
}
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);

View File

@ -17,14 +17,15 @@
import { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models';
import { FilterInfo, Filters, getFilterId } from '@shared/models/query/query.models';
import { Dashboard } from '@shared/models/dashboard.models';
import { DatasourceType, Widget } from '@shared/models/widget.models';
import { Datasource, DatasourceType, Widget } from '@shared/models/widget.models';
import {
BaseMapSettings,
MapDataLayerSettings,
MapDataSourceSettings,
mapDataSourceSettingsToDatasource,
MapType
} from '@shared/models/widget/maps/map.models';
import { WidgetExportDefinition } from '@shared/models/widget/widget-export.models';
import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition';
interface AliasFilterPair {
alias?: EntityAliasInfo,
@ -45,7 +46,7 @@ interface MapDatasourcesInfo {
additionalDataSources?: ExportDataSourceInfo;
}
export const MapExportDefinition: WidgetExportDefinition<MapDatasourcesInfo> = {
export const MapModelDefinition: WidgetModelDefinition<MapDatasourcesInfo> = {
testWidget(widget: Widget): boolean {
if (widget?.config?.settings) {
const settings = widget.config.settings;
@ -103,6 +104,26 @@ export const MapExportDefinition: WidgetExportDefinition<MapDatasourcesInfo> = {
if (info?.additionalDataSources) {
updateMapDatasourceFromExportInfo(entityAliases, filters, settings.additionalDataSources, info.additionalDataSources);
}
},
datasources(widget: Widget): Datasource[] {
const settings: BaseMapSettings = widget.config.settings as BaseMapSettings;
const datasources: Datasource[] = [];
if (settings.trips?.length) {
datasources.push(...getMapDataLayersDatasources(settings.trips));
}
if (settings.markers?.length) {
datasources.push(...getMapDataLayersDatasources(settings.markers));
}
if (settings.polygons?.length) {
datasources.push(...getMapDataLayersDatasources(settings.polygons));
}
if (settings.circles?.length) {
datasources.push(...getMapDataLayersDatasources(settings.circles));
}
if (settings.additionalDataSources?.length) {
datasources.push(...getMapDataLayersDatasources(settings.additionalDataSources));
}
return datasources;
}
};
@ -189,3 +210,16 @@ const prepareAliasAndFilterPair = (dashboard: Dashboard, settings: MapDataSource
return null;
}
}
const getMapDataLayersDatasources = (settings: MapDataLayerSettings[] | MapDataSourceSettings[]): Datasource[] => {
const datasources: Datasource[] = [];
settings.forEach((dsSettings) => {
datasources.push(mapDataSourceSettingsToDatasource(dsSettings));
if ((dsSettings as MapDataLayerSettings).additionalDataSources?.length) {
(dsSettings as MapDataLayerSettings).additionalDataSources.forEach((ds) => {
datasources.push(mapDataSourceSettingsToDatasource(ds));
});
}
});
return datasources;
};

View File

@ -14,22 +14,23 @@
/// limitations under the License.
///
import { Widget } from '@shared/models/widget.models';
import { Datasource, Widget } from '@shared/models/widget.models';
import { Dashboard } from '@shared/models/dashboard.models';
import { EntityAliases } from '@shared/models/alias.models';
import { Filters } from '@shared/models/query/query.models';
import { MapExportDefinition } from '@shared/models/widget/maps/map-export.models';
import { MapModelDefinition } from '@shared/models/widget/maps/map-model.definition';
export interface WidgetExportDefinition<T = any> {
export interface WidgetModelDefinition<T = any> {
testWidget(widget: Widget): boolean;
prepareExportInfo(dashboard: Dashboard, widget: Widget): T;
updateFromExportInfo(widget: Widget, entityAliases: EntityAliases, filters: Filters, info: T): void;
datasources(widget: Widget): Datasource[];
}
const widgetExportDefinitions: WidgetExportDefinition[] = [
MapExportDefinition
const widgetModelRegistry: WidgetModelDefinition[] = [
MapModelDefinition
];
export const getWidgetExportDefinition = (widget: Widget): WidgetExportDefinition => {
return widgetExportDefinitions.find(def => def.testWidget(widget));
export const findWidgetModelDefinition = (widget: Widget): WidgetModelDefinition => {
return widgetModelRegistry.find(def => def.testWidget(widget));
}