Browse Source

Merge pull request #15691 from dashevchenko/uiSortItemsFix

[UI sorting] Fixed UI sorting to match backend strategy
pull/15517/merge
Viacheslav Klimov 6 days ago
committed by GitHub
parent
commit
767fdbef6d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java
  2. 2
      common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java
  3. 3
      ui-ngx/src/app/core/auth/auth.models.ts
  4. 4
      ui-ngx/src/app/core/auth/auth.reducer.ts
  5. 3
      ui-ngx/src/app/core/auth/auth.service.ts
  6. 16
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts
  7. 15
      ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts
  8. 43
      ui-ngx/src/app/shared/models/page/page-link.ts

12
application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java

@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.mobile.qrCodeSettings.QrCodeSettings;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType;
import org.thingsboard.server.common.msg.edqs.EdqsService;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.mobile.QrCodeSettingService;
import org.thingsboard.server.dao.trendz.TrendzSettingsService;
@ -52,6 +53,7 @@ import org.thingsboard.server.utils.DebugModeRateLimitsConfig;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@Hidden
@ -76,6 +78,11 @@ public class SystemInfoController extends BaseController {
@Value("${debug.settings.default_duration:15}")
private int defaultDebugDurationMinutes;
@Value("${sql.entity_data_query_nulls_order_strategy:default}")
private String nullsOrderStrategy;
private static final Set<String> ACCEPTED_NULLS_ORDER_STRATEGIES = Set.of("default", "nulls_first", "nulls_last");
@Autowired(required = false)
private BuildProperties buildProperties;
@ -91,6 +98,9 @@ public class SystemInfoController extends BaseController {
@Autowired
private TrendzSettingsService trendzSettingsService;
@Autowired
private EdqsService edqsService;
@PostConstruct
public void init() {
JsonNode info = buildInfoObject();
@ -150,6 +160,8 @@ public class SystemInfoController extends BaseController {
}
systemParams.setUserSettings(userSettingsNode);
systemParams.setMaxDatapointsLimit(maxDatapointsLimit);
systemParams.setNullsOrderStrategy(ACCEPTED_NULLS_ORDER_STRATEGIES.contains(nullsOrderStrategy) ? nullsOrderStrategy : "default");
systemParams.setEdqsEnabled(edqsService.isApiEnabled());
if (!currentUser.isSystemAdmin()) {
DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration();
systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize());

2
common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java

@ -39,4 +39,6 @@ public class SystemParams {
long maxArgumentsPerCF;
long maxDataPointsPerRollingArg;
TrendzSettings trendzSettings;
String nullsOrderStrategy;
boolean edqsEnabled;
}

3
ui-ngx/src/app/core/auth/auth.models.ts

@ -17,6 +17,7 @@
import { AuthUser, User } from '@shared/models/user.model';
import { UserSettings } from '@shared/models/user-settings.models';
import { TrendzSettings } from '@shared/models/trendz-settings.models';
import { NullsOrderStrategy } from '@shared/models/page/page-link';
export interface SysParamsState {
userTokenAccessEnabled: boolean;
@ -34,6 +35,8 @@ export interface SysParamsState {
ruleChainDebugPerTenantLimitsConfiguration?: string;
calculatedFieldDebugPerTenantLimitsConfiguration?: string;
trendzSettings: TrendzSettings;
nullsOrderStrategy: NullsOrderStrategy;
edqsEnabled: boolean;
}
export interface SysParams extends SysParamsState {

4
ui-ngx/src/app/core/auth/auth.reducer.ts

@ -36,7 +36,9 @@ const emptyUserAuthState: AuthPayload = {
maxDataPointsPerRollingArg: 0,
maxDebugModeDurationMinutes: 0,
userSettings: initialUserSettings,
trendzSettings: initialTrendzSettings
trendzSettings: initialTrendzSettings,
nullsOrderStrategy: 'default',
edqsEnabled: false
};
export const initialState: AuthState = {

3
ui-ngx/src/app/core/auth/auth.service.ts

@ -20,6 +20,7 @@ import { HttpClient } from '@angular/common/http';
import { Observable, of, ReplaySubject, throwError } from 'rxjs';
import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import { setEdqsEnabled, setNullsOrderStrategy } from '@shared/models/page/page-link';
import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models';
import { Router, UrlTree } from '@angular/router';
@ -441,6 +442,8 @@ export class AuthService {
return this.http.get<SysParams>('/api/system/params', defaultHttpOptions()).pipe(
map((sysParams) => {
this.timeService.setMaxDatapointsLimit(sysParams.maxDatapointsLimit);
setNullsOrderStrategy(sysParams.nullsOrderStrategy);
setEdqsEnabled(sysParams.edqsEnabled);
return sysParams;
}),
catchError(() => of({} as SysParamsState))

16
ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts

@ -47,7 +47,7 @@ import {
isUndefined
} from '@core/utils';
import cssjs from '@core/css/css';
import { sortItems } from '@shared/models/page/page-link';
import { SortColumnType, sortItems } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order';
import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections';
import { BehaviorSubject, forkJoin, fromEvent, merge, Observable, of, Subject, Subscription } from 'rxjs';
@ -118,6 +118,7 @@ import {
dataKeyToEntityKey,
dataKeyTypeToEntityKeyType,
entityDataPageLinkSortDirection,
EntityKeyType,
KeyFilter
} from '@app/shared/models/query/query.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@ -730,8 +731,12 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.pageLink.sortOrder = null;
}
const sortOrderLabel = fromEntityColumnDef(this.sort.active, this.columns);
const sortColumnType: SortColumnType = key
? (key.type === EntityKeyType.ENTITY_FIELD || key.type === EntityKeyType.ALARM_FIELD ? 'entityField'
: key.type === EntityKeyType.TIME_SERIES ? 'timeseries' : 'attribute')
: 'entityField';
const keyFilters: KeyFilter[] = null; // TODO:
this.alarmsDatasource.loadAlarms(this.pageLink, sortOrderLabel, keyFilters);
this.alarmsDatasource.loadAlarms(this.pageLink, sortOrderLabel, sortColumnType, keyFilters);
this.ctx.detectChanges();
}
@ -1256,6 +1261,7 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
private appliedPageLink: AlarmDataPageLink;
private appliedSortOrderLabel: string;
private appliedSortColumnType: SortColumnType = 'entityField';
private reserveSpaceForHiddenAction = true;
private cellButtonActions: TableCellButtonActionDescriptor[];
@ -1294,11 +1300,13 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
this.pageDataSubject.complete();
}
loadAlarms(pageLink: AlarmDataPageLink, sortOrderLabel: string, keyFilters: KeyFilter[]) {
loadAlarms(pageLink: AlarmDataPageLink, sortOrderLabel: string,
sortColumnType: SortColumnType, keyFilters: KeyFilter[]) {
this.dataLoading = true;
// this.clear();
this.appliedPageLink = pageLink;
this.appliedSortOrderLabel = sortOrderLabel;
this.appliedSortColumnType = sortColumnType;
this.subscription.subscribeForAlarms(pageLink, keyFilters);
}
@ -1330,7 +1338,7 @@ class AlarmsDatasource implements DataSource<AlarmDataInfo> {
}
if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) {
const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC;
alarms = alarms.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc));
alarms = alarms.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc, this.appliedSortColumnType));
}
if (this.selection.hasValue()) {
const alarmIds = alarms.map((alarm) => alarm.id.id);

15
ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts

@ -100,7 +100,7 @@ import {
EntityKeyType,
KeyFilter
} from '@shared/models/query/query.models';
import { sortItems } from '@shared/models/page/page-link';
import { SortColumnType, sortItems } from '@shared/models/page/page-link';
import { entityFields } from '@shared/models/entity.models';
import { DatePipe } from '@angular/common';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
@ -617,8 +617,12 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
this.pageLink.sortOrder = null;
}
const sortOrderLabel = fromEntityColumnDef(this.sort.active, this.columns);
const sortColumnType: SortColumnType = key
? (key.type === EntityKeyType.ENTITY_FIELD ? 'entityField'
: key.type === EntityKeyType.TIME_SERIES ? 'timeseries' : 'attribute')
: 'entityField';
const keyFilters: KeyFilter[] = null; // TODO:
this.entityDatasource.loadEntities(this.pageLink, sortOrderLabel, keyFilters);
this.entityDatasource.loadEntities(this.pageLink, sortOrderLabel, sortColumnType, keyFilters);
this.ctx.detectChanges();
}
@ -865,6 +869,7 @@ class EntityDatasource implements DataSource<EntityData> {
private appliedPageLink: EntityDataPageLink;
private appliedSortOrderLabel: string;
private appliedSortColumnType: SortColumnType = 'entityField';
private reserveSpaceForHiddenAction = true;
private cellButtonActions: TableCellButtonActionDescriptor[];
@ -905,11 +910,13 @@ class EntityDatasource implements DataSource<EntityData> {
this.pageDataSubject.complete();
}
loadEntities(pageLink: EntityDataPageLink, sortOrderLabel: string, keyFilters: KeyFilter[]) {
loadEntities(pageLink: EntityDataPageLink, sortOrderLabel: string,
sortColumnType: SortColumnType, keyFilters: KeyFilter[]) {
this.dataLoading = true;
// this.clear();
this.appliedPageLink = pageLink;
this.appliedSortOrderLabel = sortOrderLabel;
this.appliedSortColumnType = sortColumnType;
this.subscription.subscribeForPaginatedData(0, pageLink, keyFilters);
}
@ -934,7 +941,7 @@ class EntityDatasource implements DataSource<EntityData> {
});
if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) {
const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC;
entities = entities.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc));
entities = entities.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc, this.appliedSortColumnType));
}
if (!dynamicWidthCellButtonActions && this.cellButtonActions.length && entities.length) {
maxCellButtonAction = entities[0].actionCellButtons.length;

43
ui-ngx/src/app/shared/models/page/page-link.ts

@ -22,6 +22,19 @@ import { EntitiesTableAction } from '@home/models/entity/entity-table-component.
export const MAX_SAFE_PAGE_SIZE = 2147483647;
export type NullsOrderStrategy = 'default' | 'nulls_first' | 'nulls_last';
let nullsOrderStrategy: NullsOrderStrategy = 'default';
let edqsEnabled = false;
export function setNullsOrderStrategy(value: NullsOrderStrategy): void {
nullsOrderStrategy = value ?? 'default';
}
export function setEdqsEnabled(value: boolean): void {
edqsEnabled = !!value;
}
export type PageLinkSearchFunction<T> = (entity: T, textSearch: string, searchProperty?: string) => boolean;
export interface PageQueryParam extends Partial<SortOrder>{
@ -73,9 +86,37 @@ const defaultPageLinkSearch: PageLinkSearchFunction<any> =
return false;
};
export function sortItems(item1: any, item2: any, property: string, asc: boolean): number {
export type SortColumnType = 'entityField' | 'attribute' | 'timeseries';
export function sortItems(item1: any, item2: any, property: string, asc: boolean,
columnType: SortColumnType = 'entityField'): number {
const item1Value = getDescendantProp(item1, property);
const item2Value = getDescendantProp(item2, property);
const item1Empty = item1Value === null || item1Value === undefined || item1Value === '';
const item2Empty = item2Value === null || item2Value === undefined || item2Value === '';
// Mirror backend's nulls ordering. EDQS uses fixed NULLS FIRST regardless of strategy and
// naive compare below already matches it, so skip this branch when EDQS is on.
// For entityField columns the ORDER BY hits a real nullable DB column → strategy always applies.
// For attribute/timeseries the strategy only applies to numeric/boolean values; string/json
// are coalesced to '' on the backend, so naive compare below already matches its order.
if (!edqsEnabled && (item1Empty || item2Empty) && !(item1Empty && item2Empty)) {
let applyStrategy = columnType === 'entityField';
if (!applyStrategy) {
const other = item1Empty ? item2Value : item1Value;
applyStrategy =
typeof other === 'boolean' || other === 'true' || other === 'false' ||
(typeof other === 'number' && isFinite(other)) ||
(typeof other === 'string' && other.trim() !== '' && !isNaN(Number(other)));
}
if (applyStrategy) {
const nullsFirst = nullsOrderStrategy === 'nulls_first'
|| (nullsOrderStrategy === 'default' && !asc);
if (item1Empty) {
return nullsFirst ? -1 : 1;
}
return nullsFirst ? 1 : -1;
}
}
let result = 0;
if (item1Value !== item2Value) {
const item1Type = typeof item1Value;

Loading…
Cancel
Save