Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { of, Observable } from 'rxjs';
import { DataService } from 'src/app/services/data.service';
import { RestClientService } from 'src/app/services/rest-client.service';
import { DataModelBase } from '../Base';
import { SimpleCollection, SimpleCollectionWithParent } from './CollectionBase';

// Mock raw data type
interface IRawTestItem {
Id: string;
Name: string;
}

// Mock model class
class TestModel extends DataModelBase<IRawTestItem> {
constructor(data: DataService, raw: IRawTestItem) {
super(data, raw);
}

public get uniqueId(): string {
return this.raw.Id;
}
}

// Mock model class with parent
class TestModelWithParent extends DataModelBase<IRawTestItem> {
constructor(data: DataService, raw: IRawTestItem, public parent: { id: string }) {
super(data, raw, parent);
}

public get uniqueId(): string {
return this.raw.Id;
}
}

// Mock parent type
interface ITestParent {
id: string;
name: string;
}

describe('CollectionBase', () => {
const restClientMock: RestClientService = {} as RestClientService;

const mockDataService: DataService = {
restClient: restClientMock,
} as DataService;

describe('SimpleCollection', () => {
let collection: SimpleCollection<TestModel, IRawTestItem>;
let mockFetchFn: (data: DataService) => Observable<IRawTestItem[]>;
const mockRawItems: IRawTestItem[] = [
{ Id: '1', Name: 'Item 1' },
{ Id: '2', Name: 'Item 2' },
{ Id: '3', Name: 'Item 3' }
];

beforeEach(() => {
mockFetchFn = jasmine.createSpy('fetchFn').and.returnValue(of(mockRawItems));
collection = new SimpleCollection<TestModel, IRawTestItem>(
mockDataService,
TestModel,
mockFetchFn
);
});

it('should create a collection instance', () => {
expect(collection).toBeTruthy();
expect(collection.length).toBe(0);
expect(collection.isInitialized).toBe(false);
});

it('should fetch and map items on refresh', async () => {
await collection.refresh().toPromise();

expect(mockFetchFn).toHaveBeenCalled();
expect(collection.length).toBe(3);
expect(collection.isInitialized).toBe(true);
});

it('should create correct model instances', async () => {
await collection.refresh().toPromise();

expect(collection.collection[0]).toBeInstanceOf(TestModel);
expect(collection.collection[0].raw.Id).toBe('1');
expect(collection.collection[0].raw.Name).toBe('Item 1');
});

it('should find items by uniqueId', async () => {
await collection.refresh().toPromise();

const found = collection.find('2');
expect(found).toBeTruthy();
expect(found.raw.Name).toBe('Item 2');
});

it('should return null for non-existing uniqueId', async () => {
await collection.refresh().toPromise();

const found = collection.find('999');
expect(found).toBeFalsy();
});
});

describe('SimpleCollectionWithParent', () => {
let collection: SimpleCollectionWithParent<TestModelWithParent, IRawTestItem, ITestParent>;
let mockFetchFn: (data: DataService, parent: ITestParent) => Observable<IRawTestItem[]>;
const mockParent: ITestParent = { id: 'parent-1', name: 'Parent' };
const mockRawItems: IRawTestItem[] = [
{ Id: 'p1-1', Name: 'Item 1' },
{ Id: 'p1-2', Name: 'Item 2' }
];

beforeEach(() => {
mockFetchFn = jasmine.createSpy('fetchFn').and.returnValue(of(mockRawItems));
collection = new SimpleCollectionWithParent<TestModelWithParent, IRawTestItem, ITestParent>(
mockDataService,
mockParent,
TestModelWithParent,
mockFetchFn
);
});

it('should create a collection instance with parent', () => {
expect(collection).toBeTruthy();
expect(collection.parent).toBe(mockParent);
expect(collection.length).toBe(0);
expect(collection.isInitialized).toBe(false);
});

it('should fetch items using parent in fetch function', async () => {
await collection.refresh().toPromise();

expect(mockFetchFn).toHaveBeenCalled();
expect(collection.length).toBe(2);
});

it('should create model instances with parent reference', async () => {
await collection.refresh().toPromise();

expect(collection.collection[0]).toBeInstanceOf(TestModelWithParent);
expect(collection.collection[0].parent).toBe(mockParent);
});

it('should handle empty collections', async () => {
mockFetchFn = jasmine.createSpy('fetchFn').and.returnValue(of([]));
collection = new SimpleCollectionWithParent<TestModelWithParent, IRawTestItem, ITestParent>(
mockDataService,
mockParent,
TestModelWithParent,
mockFetchFn
);

await collection.refresh().toPromise();

expect(collection.length).toBe(0);
expect(collection.isInitialized).toBe(true);
});
});
});
67 changes: 67 additions & 0 deletions src/SfxWeb/src/app/Models/DataModels/collections/CollectionBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,70 @@ export class DataModelCollectionBase<T extends IDataModel<any>> implements IData
return null;
}
}

/**
* Type definition for a model constructor that takes DataService and raw data only.
*/
export type ModelConstructorNoParent<T, R> = new (data: DataService, raw: R) => T;

/**
* Type definition for a model constructor that takes DataService, raw data, and a parent.
*/
export type ModelConstructorWithParent<T, R, P> = new (data: DataService, raw: R, parent: P) => T;

/**
* A generic collection class that reduces boilerplate for simple collections without a parent.
*
* This class handles the common pattern of fetching data via REST API and mapping to model instances.
*
* @example
* class MyCollection extends SimpleCollection<MyModel, IRawItem> {
* constructor(data: DataService) {
* super(data, MyModel, (data, messageHandler) => data.restClient.getItems(messageHandler));
* }
* }
*/
export class SimpleCollection<T extends IDataModel<R>, R> extends DataModelCollectionBase<T> {
constructor(
data: DataService,
protected modelClass: ModelConstructorNoParent<T, R>,
protected fetchFn: (data: DataService, messageHandler?: IResponseMessageHandler) => Observable<R[]>
) {
super(data);
}

protected retrieveNewCollection(messageHandler?: IResponseMessageHandler): Observable<T[]> {
return this.fetchFn(this.data, messageHandler).pipe(
map(rawItems => rawItems.map(raw => new this.modelClass(this.data, raw)))
);
}
}

/**
* A generic collection class that reduces boilerplate for simple collections with a parent.
*
* This class handles the common pattern of fetching data via REST API and mapping to model instances.
*
* @example
* class MyCollection extends SimpleCollectionWithParent<MyModel, IRawItem, ParentType> {
* constructor(data: DataService, parent: ParentType) {
* super(data, parent, MyModel, (data, parent, messageHandler) => data.restClient.getItems(parent.id, messageHandler));
* }
* }
*/
export class SimpleCollectionWithParent<T extends IDataModel<R>, R, P> extends DataModelCollectionBase<T> {
constructor(
data: DataService,
public parent: P,
protected modelClass: ModelConstructorWithParent<T, R, P>,
protected fetchFn: (data: DataService, parent: P, messageHandler?: IResponseMessageHandler) => Observable<R[]>
) {
super(data, parent);
}

protected retrieveNewCollection(messageHandler?: IResponseMessageHandler): Observable<T[]> {
return this.fetchFn(this.data, this.parent, messageHandler).pipe(
map(rawItems => rawItems.map(raw => new this.modelClass(this.data, raw, this.parent)))
);
}
}
107 changes: 50 additions & 57 deletions src/SfxWeb/src/app/Models/DataModels/collections/Collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { IClusterHealthChunk, IDeployedServicePackageHealthStateChunk } from '..
import { IResponseMessageHandler } from 'src/app/Common/ResponseMessageHandlers';
import { Observable, of, throwError } from 'rxjs';
import { ValueResolver, ITextAndBadge } from 'src/app/Utils/ValueResolver';
import { IRawDeployedServicePackage } from '../../RawDataTypes';
import { IRawDeployedServicePackage, IRawBackupPolicy, IRawPartitionBackup, IRawApplicationBackupConfigurationInfo, IRawServiceBackupConfigurationInfo, IRawPartition, IRawReplicaOnPartition, IRawService } from '../../RawDataTypes';
import { IdGenerator } from 'src/app/Utils/IdGenerator';
import { DataService } from 'src/app/services/data.service';
import { HealthStateConstants } from 'src/app/Common/Constants';
Expand All @@ -22,19 +22,21 @@ import { FabricEventBase, FabricEventInstanceModel, ClusterEvent, NodeEvent, App
import { ListSettings, ListColumnSetting, ListColumnSettingWithFilter, ListColumnSettingWithEventStoreFullDescription, ListColumnSettingWithEventStoreRowDisplay } from '../../ListSettings';
import { TimeUtils } from 'src/app/Utils/TimeUtils';
import { PartitionBackup, PartitionBackupInfo } from '../PartitionBackupInfo';
import { DataModelCollectionBase, IDataModelCollection } from './CollectionBase';
import { DataModelCollectionBase, IDataModelCollection, SimpleCollection, SimpleCollectionWithParent } from './CollectionBase';
import groupBy from 'lodash/groupBy';
import { RoutesService } from 'src/app/services/routes.service';
// -----------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License file under the project root for license information.
// -----------------------------------------------------------------------------

export class BackupPolicyCollection extends DataModelCollectionBase<BackupPolicy> {
protected retrieveNewCollection(messageHandler?: IResponseMessageHandler): Observable<any> {
return this.data.restClient.getBackupPolicies(messageHandler).pipe(map(items => {
return items.map(raw => new BackupPolicy(this.data, raw));
}));
/**
* BackupPolicyCollection using generic SimpleCollection pattern.
* Fetches backup policies from REST API and maps to BackupPolicy model instances.
*/
export class BackupPolicyCollection extends SimpleCollection<BackupPolicy, IRawBackupPolicy> {
constructor(data: DataService) {
super(data, BackupPolicy, (dataService, messageHandler) => dataService.restClient.getBackupPolicies(messageHandler));
}
}

Expand Down Expand Up @@ -136,29 +138,25 @@ export class ApplicationTypeGroupCollection extends DataModelCollectionBase<Appl

}

export class ApplicationBackupConfigurationInfoCollection extends DataModelCollectionBase<ApplicationBackupConfigurationInfo> {
public constructor(data: DataService, public parent: Application) {
super(data, parent);
}

protected retrieveNewCollection(messageHandler?: IResponseMessageHandler): Observable<any> {
return this.data.restClient.getApplicationBackupConfigurationInfoCollection(this.parent.id, messageHandler)
.pipe(map(items => {
return items.map(raw => new ApplicationBackupConfigurationInfo(this.data, raw, this.parent));
}));
/**
* ApplicationBackupConfigurationInfoCollection using generic SimpleCollectionWithParent pattern.
* Fetches application backup configuration info from REST API and maps to model instances.
*/
export class ApplicationBackupConfigurationInfoCollection extends SimpleCollectionWithParent<ApplicationBackupConfigurationInfo, IRawApplicationBackupConfigurationInfo, Application> {
constructor(data: DataService, parent: Application) {
super(data, parent, ApplicationBackupConfigurationInfo,
(dataService, parentApp, messageHandler) => dataService.restClient.getApplicationBackupConfigurationInfoCollection(parentApp.id, messageHandler));
}
}

export class ServiceBackupConfigurationInfoCollection extends DataModelCollectionBase<ServiceBackupConfigurationInfo> {
public constructor(data: DataService, public parent: Service) {
super(data, parent);
}

protected retrieveNewCollection(messageHandler?: IResponseMessageHandler): Observable<any> {
return this.data.restClient.getServiceBackupConfigurationInfoCollection(this.parent.id, messageHandler)
.pipe(map(items => {
return items.map(raw => new ServiceBackupConfigurationInfo(this.data, raw, this.parent));
}));
/**
* ServiceBackupConfigurationInfoCollection using generic SimpleCollectionWithParent pattern.
* Fetches service backup configuration info from REST API and maps to model instances.
*/
export class ServiceBackupConfigurationInfoCollection extends SimpleCollectionWithParent<ServiceBackupConfigurationInfo, IRawServiceBackupConfigurationInfo, Service> {
constructor(data: DataService, parent: Service) {
super(data, parent, ServiceBackupConfigurationInfo,
(dataService, parentService, messageHandler) => dataService.restClient.getServiceBackupConfigurationInfoCollection(parentService.id, messageHandler));
}
}

Expand All @@ -179,16 +177,14 @@ export class PartitionBackupCollection extends DataModelCollectionBase<Partition
}
}

export class SinglePartitionBackupCollection extends DataModelCollectionBase<PartitionBackup> {
public constructor(data: DataService, public parent: PartitionBackupInfo) {
super(data, parent);
}

public retrieveNewCollection(messageHandler?: IResponseMessageHandler): Observable<any> {
return this.data.restClient.getLatestPartitionBackup(this.parent.parent.id, messageHandler)
.pipe(map(items => {
return items.map(raw => new PartitionBackup(this.data, raw, this.parent));
}));
/**
* SinglePartitionBackupCollection using generic SimpleCollectionWithParent pattern.
* Fetches the latest partition backup from REST API and maps to model instances.
*/
export class SinglePartitionBackupCollection extends SimpleCollectionWithParent<PartitionBackup, IRawPartitionBackup, PartitionBackupInfo> {
constructor(data: DataService, parent: PartitionBackupInfo) {
super(data, parent, PartitionBackup,
(dataService, parentInfo, messageHandler) => dataService.restClient.getLatestPartitionBackup(parentInfo.parent.id, messageHandler));
}
}

Expand All @@ -215,9 +211,14 @@ export class ServiceTypeCollection extends DataModelCollectionBase<ServiceType>
}
}

export class PartitionCollection extends DataModelCollectionBase<Partition> {
public constructor(data: DataService, public parent: Service) {
super(data, parent);
/**
* PartitionCollection using generic SimpleCollectionWithParent pattern.
* Extends with mergeClusterHealthStateChunk for health state management.
*/
export class PartitionCollection extends SimpleCollectionWithParent<Partition, IRawPartition, Service> {
constructor(data: DataService, parent: Service) {
super(data, parent, Partition,
(dataService, parentService, messageHandler) => dataService.restClient.getPartitions(parentService.parent.id, parentService.id, messageHandler));
}

public mergeClusterHealthStateChunk(clusterHealthChunk: IClusterHealthChunk): Observable<any> {
Expand All @@ -230,18 +231,17 @@ export class PartitionCollection extends DataModelCollectionBase<Partition> {
}
return of(true);
}

protected retrieveNewCollection(messageHandler?: IResponseMessageHandler): Observable<any> {
return this.data.restClient.getPartitions(this.parent.parent.id, this.parent.id, messageHandler)
.pipe(map(items => {
return items.map(raw => new Partition(this.data, raw, this.parent));
}));
}
}

export class ReplicaOnPartitionCollection extends DataModelCollectionBase<ReplicaOnPartition> {
public constructor(data: DataService, public parent: Partition) {
super(data, parent);
/**
* ReplicaOnPartitionCollection using generic SimpleCollectionWithParent pattern.
* Extends with additional getters for service type information.
*/
export class ReplicaOnPartitionCollection extends SimpleCollectionWithParent<ReplicaOnPartition, IRawReplicaOnPartition, Partition> {
constructor(data: DataService, parent: Partition) {
super(data, parent, ReplicaOnPartition,
(dataService, parentPartition, messageHandler) => dataService.restClient.getReplicasOnPartition(
parentPartition.parent.parent.id, parentPartition.parent.id, parentPartition.id, messageHandler));
}

public get isStatefulService(): boolean {
Expand All @@ -251,13 +251,6 @@ export class ReplicaOnPartitionCollection extends DataModelCollectionBase<Replic
public get isStatelessService(): boolean {
return this.parent.isStatelessService;
}

protected retrieveNewCollection(messageHandler?: IResponseMessageHandler): Observable<any> {
return this.data.restClient.getReplicasOnPartition(this.parent.parent.parent.id, this.parent.parent.id, this.parent.id, messageHandler)
.pipe(map(items => {
return items.map(raw => new ReplicaOnPartition(this.data, raw, this.parent));
}));
}
}

export class DeployedServicePackageCollection extends DataModelCollectionBase<DeployedServicePackage> {
Expand Down
Loading
Loading