Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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)))
);
}
}
70 changes: 33 additions & 37 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, IRawBackupConfigurationInfo, IRawPartitionBackup, IRawApplicationBackupConfigurationInfo, IRawServiceBackupConfigurationInfo } from '../../RawDataTypes';
Comment thread
sridmad marked this conversation as resolved.
Outdated
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 Down
Loading