From 7299f41527128af83fa0a44b41827212c7a50563 Mon Sep 17 00:00:00 2001 From: Shiran Pasternak Date: Thu, 28 Sep 2023 15:45:16 -0400 Subject: [PATCH 1/4] Sets proper name on record type for identification Also supports a custom name on the `@Model()` decorator. --- desktop/src/@batch-flask/core/record/decorators.ts | 7 ++++--- .../src/@batch-flask/core/record/record.spec.ts | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/desktop/src/@batch-flask/core/record/decorators.ts b/desktop/src/@batch-flask/core/record/decorators.ts index 92f5c8f895..628d77f593 100644 --- a/desktop/src/@batch-flask/core/record/decorators.ts +++ b/desktop/src/@batch-flask/core/record/decorators.ts @@ -56,13 +56,12 @@ export function ListProp(type: any) { }; } -export function Model() { +export function Model(name?: string) { return {}>(ctr: T) => { if (!(ctr.prototype instanceof Record)) { throw new RecordMissingExtendsError(ctr); } - - return (class extends ctr { + const model = (class extends ctr { constructor(...args: any[]) { const [data] = args; if (data instanceof ctr) { @@ -72,5 +71,7 @@ export function Model() { (this as any)._completeInitialization(); } }); + Object.defineProperty(model, "name", { value: name || ctr.name }); + return model; }; } diff --git a/desktop/src/@batch-flask/core/record/record.spec.ts b/desktop/src/@batch-flask/core/record/record.spec.ts index 1634d61473..3cfab632f8 100644 --- a/desktop/src/@batch-flask/core/record/record.spec.ts +++ b/desktop/src/@batch-flask/core/record/record.spec.ts @@ -44,6 +44,12 @@ class InheritedTestRec extends SimpleTestRec { public d: number; } +@Model("CustomName") +class CustomNameRec extends Record { + @Prop() + public id: string = "default-id"; +} + describe("Record", () => { it("should throw an exeption when record doesn't extends Record class", () => { try { @@ -176,4 +182,12 @@ describe("Record", () => { expect(SimpleTestRec.isStaticMethod).not.toBeFalsy(); expect(SimpleTestRec.isStaticMethod()).toBe(true); }); + + it("should allow a custom record name to be set", () => { + const rec1 = new TestRec(); + expect(rec1.constructor.name).toEqual("TestRec"); + + const rec2 = new CustomNameRec(); + expect(rec2.constructor.name).toEqual("CustomName"); + }); }); From c6efb9cfe97f9c9e6efadc15694f489245fdd9cd Mon Sep 17 00:00:00 2001 From: Shiran Pasternak Date: Thu, 21 Sep 2023 16:47:01 -0400 Subject: [PATCH 2/4] Better typing for storage-related modules --- .../file-group-picker.component.ts | 1 - desktop/src/app/models/blob-container.ts | 4 +- .../services/core/data/storage-list-getter.ts | 7 ++- .../storage/blob-storage-client-proxy.ts | 3 +- .../services/storage/models/storage-blob.ts | 56 ++++++++++--------- .../services/storage/storage-blob.service.ts | 18 +++--- .../storage/storage-client.service.ts | 10 +++- .../storage/storage-container.service.ts | 4 +- 8 files changed, 57 insertions(+), 46 deletions(-) diff --git a/desktop/src/app/components/data/shared/file-group-picker/file-group-picker.component.ts b/desktop/src/app/components/data/shared/file-group-picker/file-group-picker.component.ts index 22829fc851..18d94e0b26 100644 --- a/desktop/src/app/components/data/shared/file-group-picker/file-group-picker.component.ts +++ b/desktop/src/app/components/data/shared/file-group-picker/file-group-picker.component.ts @@ -4,7 +4,6 @@ import { import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, } from "@angular/forms"; -import { MatOptionSelectionChange } from "@angular/material/core"; import { FilterBuilder, ListView } from "@batch-flask/core"; import { Activity, DialogService } from "@batch-flask/ui"; import { FileGroupCreateFormComponent } from "app/components/data/action"; diff --git a/desktop/src/app/models/blob-container.ts b/desktop/src/app/models/blob-container.ts index 341270c73d..f8de3b3674 100644 --- a/desktop/src/app/models/blob-container.ts +++ b/desktop/src/app/models/blob-container.ts @@ -8,7 +8,7 @@ export interface BlobContainerAttributes { name: string; publicAccessLevel: string; metadata?: any; - lastModified: Date; + lastModified?: Date; lease?: Partial; } @@ -25,7 +25,7 @@ export class BlobContainer extends Record implements Na @Prop() public publicAccessLevel: string; @Prop() public metadata: any; - @Prop() public lastModified: Date; + @Prop() public lastModified?: Date; @Prop() public lease: ContainerLease; @Prop() public storageAccountId: string; diff --git a/desktop/src/app/services/core/data/storage-list-getter.ts b/desktop/src/app/services/core/data/storage-list-getter.ts index 268797cd4a..b6c1387b88 100644 --- a/desktop/src/app/services/core/data/storage-list-getter.ts +++ b/desktop/src/app/services/core/data/storage-list-getter.ts @@ -1,5 +1,6 @@ import { Type } from "@angular/core"; import { ContinuationToken, ListGetter, ListGetterConfig, Record, ServerError } from "@batch-flask/core"; +import { BlobStorageClientProxy } from "app/services/storage"; import { StorageBlobResult } from "app/services/storage/models"; import { StorageClientService } from "app/services/storage/storage-client.service"; import { Observable, from, throwError } from "rxjs"; @@ -12,13 +13,15 @@ export interface StorageBaseParams { export interface StorageListConfig, TParams extends StorageBaseParams> extends ListGetterConfig { - getData: (client: any, params: TParams, options: any, token: any) => Promise>; + getData: (client: BlobStorageClientProxy, params: TParams, options: any, token: any) => + Promise>; } export class StorageListGetter, TParams extends StorageBaseParams> extends ListGetter { - private _getData: (client: any, params: TParams, options: any, token: any) => Promise>; + private _getData: (client: BlobStorageClientProxy, params: TParams, options: any, token: any) => + Promise>; constructor( type: Type, diff --git a/desktop/src/app/services/storage/blob-storage-client-proxy.ts b/desktop/src/app/services/storage/blob-storage-client-proxy.ts index 3ae96a6013..e3241f8dda 100644 --- a/desktop/src/app/services/storage/blob-storage-client-proxy.ts +++ b/desktop/src/app/services/storage/blob-storage-client-proxy.ts @@ -4,6 +4,7 @@ import { IpcEvent } from "common/constants"; import { SharedAccessPolicy } from "./models"; import * as blob from "./models/storage-blob"; import { BlobContentResult } from "./storage-blob.service"; +import { StorageClient } from "./storage-client.service"; const storageIpc = IpcEvent.storageBlob; @@ -18,7 +19,7 @@ export interface ListBlobResponse { }; } -export class BlobStorageClientProxy { +export class BlobStorageClientProxy implements StorageClient { private storageInfo: { url: string; account: string, key: string }; diff --git a/desktop/src/app/services/storage/models/storage-blob.ts b/desktop/src/app/services/storage/models/storage-blob.ts index 7cf2cf58f7..4042ac914e 100644 --- a/desktop/src/app/services/storage/models/storage-blob.ts +++ b/desktop/src/app/services/storage/models/storage-blob.ts @@ -1,10 +1,38 @@ -import { ContainerItem, BlobUploadCommonResponse, ContainerGetPropertiesResponse, CommonOptions } from "@azure/storage-blob"; +import { BlobUploadCommonResponse, ContainerGetPropertiesResponse, CommonOptions, ContainerItem } from "@azure/storage-blob"; import { Model, Prop, Record } from "@batch-flask/core"; import { SharedAccessPolicy } from "./shared-access-policy"; +import { BlobContainer } from "app/models"; // Placeholder; we don't use any options to storage-blob API requests export type RequestOptions = Partial; +export interface BlobProperties { + name: string; + url: string; + isDirectory: boolean; + properties: { + contentLength: number; + contentType: string; + creationTime: Date | string; + lastModified?: Date | string; + }; +} + +export type ContainerProperties = ContainerGetPropertiesResponse; + +@Model() +export class BlobItem extends Record { + @Prop() name: string; + @Prop() url: string; + @Prop() isDirectory: boolean; + @Prop() properties: { + contentLength: number; + contentType: string; + creationTime: Date | string; + lastModified?: Date | string; + } +} + export interface BaseParams { url: string; account: string; @@ -94,32 +122,6 @@ export interface GetBlobContentResult { content: string; } -export interface BlobProperties { - name: string; - url: string; - isDirectory: boolean; - properties: { - contentLength: number; - contentType: string; - creationTime: Date | string; - lastModified: Date | string; - }; -} - -@Model() -export class BlobItem extends Record { - @Prop() name: string; - @Prop() url: string; - @Prop() isDirectory: boolean; - @Prop() properties: { - contentLength: number; - contentType: string; - creationTime: Date | string; - lastModified: Date | string; - } -} - -export type ContainerProperties = ContainerGetPropertiesResponse; export interface ListBlobOptions { /** diff --git a/desktop/src/app/services/storage/storage-blob.service.ts b/desktop/src/app/services/storage/storage-blob.service.ts index 3e0a4f9b0b..7d1b91bb3a 100644 --- a/desktop/src/app/services/storage/storage-blob.service.ts +++ b/desktop/src/app/services/storage/storage-blob.service.ts @@ -19,7 +19,7 @@ import { Constants } from "common"; import { AsyncSubject, Observable, from, of, throwError } from "rxjs"; import { catchError, concat, concatMap, map, share, switchMap, take } from "rxjs/operators"; import { BlobStorageClientProxy } from "./blob-storage-client-proxy"; -import { StorageClientService } from "./storage-client.service"; +import { StorageClient, StorageClientService } from "./storage-client.service"; export interface ListBlobParams { storageAccountId: string; @@ -42,7 +42,10 @@ export interface BlobContentResult { content: string; } -export type StorageContainerProperties = ContainerProperties; +export interface StorageContainerProperties extends Omit { + lastModified?: Date; + etag?: string; +} export interface NavigateBlobsOptions { /** @@ -117,8 +120,7 @@ export class StorageBlobService { this._blobListGetter = new StorageListGetter(FileRecord, this.storageClient, { cache: (params) => this.getBlobFileCache(params), - getData: (client: BlobStorageClientProxy, - params, options, continuationToken) => { + getData: async (client: StorageClient, params, options, continuationToken) => { const blobOptions: ListBlobOptions = { folder: options.original.folder, recursive: options.original.recursive, @@ -126,12 +128,8 @@ export class StorageBlobService { maxPageSize: this.maxBlobPageSize }; - // N.B. `BlobItem` and `FileRecord` are nearly identical - return client.listBlobs( - params.container, - blobOptions, - continuationToken, - ) as Promise>; + const blobs = await client.listBlobs(params.container, blobOptions, continuationToken); + return { data: blobs.data.map(blob => new FileRecord(blob)) }; }, logIgnoreError: storageIgnoredErrors, }); diff --git a/desktop/src/app/services/storage/storage-client.service.ts b/desktop/src/app/services/storage/storage-client.service.ts index f879dd6c3c..a2ba915167 100644 --- a/desktop/src/app/services/storage/storage-client.service.ts +++ b/desktop/src/app/services/storage/storage-client.service.ts @@ -1,7 +1,7 @@ import { Injectable } from "@angular/core"; import { ServerError } from "@batch-flask/core"; import { ElectronRemote } from "@batch-flask/electron"; -import { StorageKeys } from "app/models"; +import { BlobContainer, StorageKeys } from "app/models"; import { BatchExplorerService } from "app/services/batch-explorer.service"; import { ArmResourceUtils } from "app/utils"; import { Observable, throwError } from "rxjs"; @@ -10,6 +10,7 @@ import { BatchAccountService } from "../batch-account"; import { BlobStorageClientProxy } from "./blob-storage-client-proxy"; import { StorageAccountKeysService } from "./storage-account-keys.service"; import { StorageClientProxyFactory } from "./storage-client-proxy-factory"; +import { ListBlobsResult, ListContainersResult, StorageBlobResult } from "./models"; export interface AutoStorageSettings { lastKeySync: Date; @@ -23,6 +24,13 @@ export interface StorageKeyCachedItem { keys: StorageKeys; } +export interface StorageClient { + listContainersWithPrefix(prefix: string, continuationToken?: string, options?: any): + Promise; + listBlobs(containerName: string, options: any, continuationToken?: string): + Promise; +} + @Injectable({ providedIn: "root" }) export class StorageClientService { public hasAutoStorage: Observable; diff --git a/desktop/src/app/services/storage/storage-container.service.ts b/desktop/src/app/services/storage/storage-container.service.ts index a08887577c..4323f19ab5 100644 --- a/desktop/src/app/services/storage/storage-container.service.ts +++ b/desktop/src/app/services/storage/storage-container.service.ts @@ -14,7 +14,7 @@ import { SharedAccessPolicy } from "app/services/storage/models"; import { Observable, Subject, from, throwError } from "rxjs"; import { catchError, flatMap, share } from "rxjs/operators"; import { BlobStorageClientProxy } from "./blob-storage-client-proxy"; -import { StorageClientService } from "./storage-client.service"; +import { StorageClient, StorageClientService } from "./storage-client.service"; export interface GetContainerParams { storageAccountId: string; @@ -62,7 +62,7 @@ export class StorageContainerService { }); this._containerListGetter = new StorageListGetter(BlobContainer, this.storageClient, { cache: params => this._containerCache.getCache(params), - getData: async (client, params, options, continuationToken) => { + getData: async (client: StorageClient, params, options, continuationToken) => { let prefix = null; if (options && options.filter) { prefix = options.filter.value; From f0e6615b398c5f4ed388f584af71ba74a25cba3f Mon Sep 17 00:00:00 2001 From: Shiran Pasternak Date: Thu, 26 Oct 2023 23:31:09 -0400 Subject: [PATCH 3/4] Proper token wrapper for Python RPC ARM calls --- desktop/python/server/aad_auth.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/desktop/python/server/aad_auth.py b/desktop/python/server/aad_auth.py index d647a56c8a..f51431f14e 100644 --- a/desktop/python/server/aad_auth.py +++ b/desktop/python/server/aad_auth.py @@ -1,5 +1,14 @@ import azext.batch from msrestazure.azure_active_directory import AdalAuthentication +from azure.core.credentials import TokenCredential, AccessToken + +class BatchExplorerTokenCredential(TokenCredential): + def __init__(self, access_token) -> None: + super().__init__() + self.access_token = access_token + + def get_token(self, *scopes, **kwargs): + return AccessToken(token=self.access_token, expires_on=0) class BatchAccount: def __init__(self, account_id: str, name: str, account_endpoint: str, subscription_id: str): @@ -22,8 +31,8 @@ class AADAuth: def __init__(self, batchToken: str, armToken: str, armUrl: str, storage_endpoint: str, account: BatchAccount): self.batchCreds = AdalAuthentication( lambda: {'accessToken': batchToken, 'tokenType': 'Bearer'}) - self.armCreds = AdalAuthentication( - lambda: {'accessToken': armToken, 'tokenType': 'Bearer'}) + + self.armCreds = BatchExplorerTokenCredential(armToken) self.armUrl = armUrl self.storage_endpoint = storage_endpoint self.account = account From 9b886cea0d2ed24196673d87a9fc4c62ae0663fc Mon Sep 17 00:00:00 2001 From: Shiran Pasternak Date: Thu, 21 Sep 2023 16:47:01 -0400 Subject: [PATCH 4/4] Fixes inoperable file group controls Bug introduced in #2745: No handler for a file group that has changed. Also restores the picker option labels. Backfills container names on certain API calls because they are not returned, but are needed by various UI flows. --- .../targeted-data-cache.ts | 2 +- .../file-group-picker.component.spec.ts | 92 +++++++++++++++++++ .../file-group-picker.component.ts | 25 +++-- .../file-group-picker/file-group-picker.html | 5 +- .../submit/market-application.model.ts | 7 +- .../parameter-input.component.ts | 9 +- .../submit/submit-ncj-template.component.ts | 2 +- ...ner-configuration-picker.component.spec.ts | 4 +- .../app/models/azure-batch/pool/pool.spec.ts | 4 +- desktop/src/app/models/blob-container.ts | 2 +- .../storage/blob-storage-client-proxy.ts | 7 +- .../services/storage/models/storage-blob.ts | 6 +- .../storage/storage-container.service.ts | 7 +- 13 files changed, 141 insertions(+), 31 deletions(-) create mode 100644 desktop/src/app/components/data/shared/file-group-picker/file-group-picker.component.spec.ts diff --git a/desktop/src/@batch-flask/core/data/targeted-data-cache/targeted-data-cache.ts b/desktop/src/@batch-flask/core/data/targeted-data-cache/targeted-data-cache.ts index b79926aaf2..6682d15d10 100644 --- a/desktop/src/@batch-flask/core/data/targeted-data-cache/targeted-data-cache.ts +++ b/desktop/src/@batch-flask/core/data/targeted-data-cache/targeted-data-cache.ts @@ -27,7 +27,7 @@ export class TargetedDataCache> { * Return the key of the cache associated to the given params */ public getCacheKey(params: TParams) { - return this._options.key!(params); + return this._options.key?.call(null, params); } public getCache(params: TParams): DataCache { diff --git a/desktop/src/app/components/data/shared/file-group-picker/file-group-picker.component.spec.ts b/desktop/src/app/components/data/shared/file-group-picker/file-group-picker.component.spec.ts new file mode 100644 index 0000000000..332b21152e --- /dev/null +++ b/desktop/src/app/components/data/shared/file-group-picker/file-group-picker.component.spec.ts @@ -0,0 +1,92 @@ +import { HttpClientTestingModule } from "@angular/common/http/testing"; +import { Component, DebugElement } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { FormControl, FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { By } from "@angular/platform-browser"; +import { BrowserDynamicTestingModule } + from "@angular/platform-browser-dynamic/testing"; +import { RouterTestingModule } from "@angular/router/testing"; +import { GlobalStorage, USER_SERVICE, UserConfigurationService } + from "@batch-flask/core"; +import { + I18nTestingModule, + MockGlobalStorage, + MockUserConfigurationService +} from "@batch-flask/core/testing"; +import { ElectronTestingModule } from "@batch-flask/electron/testing"; +import { ButtonsModule, DialogService, FormModule, SelectModule } + from "@batch-flask/ui"; +import { AuthService, BatchExplorerService } from "app/services"; +import { BehaviorSubject } from "rxjs"; +import { FileGroupPickerComponent } from "./file-group-picker.component"; +import { FileGroupPickerModule } from "./file-group-picker.module"; + +@Component({ + template: ``, +}) +class TestComponent { + public control = new FormControl(); +} + +describe("FileGroupPickerComponent", () => { + let testComponent: TestComponent; + let component: FileGroupPickerComponent; + let fixture: ComponentFixture; + let de: DebugElement; + + const userServiceSpy = { currentUser: new BehaviorSubject(null) }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + FormModule, + FormsModule, + ReactiveFormsModule, + SelectModule, + RouterTestingModule, + I18nTestingModule, + HttpClientTestingModule, + ButtonsModule, + ElectronTestingModule, + BrowserDynamicTestingModule, + FileGroupPickerModule + ], + declarations: [TestComponent], + providers: [ + { provide: BatchExplorerService, useValue: {} }, + { provide: UserConfigurationService, useValue: + new MockUserConfigurationService({}) }, + { provide: AuthService, useValue: userServiceSpy }, + { provide: GlobalStorage, useValue: new MockGlobalStorage() }, + { provide: USER_SERVICE, useValue: userServiceSpy }, + { provide: DialogService, useValue: {} }, + ] + }).compileComponents(); + + fixture = TestBed.createComponent(TestComponent); + testComponent = fixture.componentInstance; + de = fixture.debugElement.query(By.css("bl-file-group-picker")); + component = fixture.debugElement.query(By.css("bl-file-group-picker")) + .componentInstance; + fixture.detectChanges(); + }); + + it("should pick an existing value", () => { + const testValue = 'existingValue'; + component.fileGroupPicked(testValue); + fixture.detectChanges(); + expect(component.value.value).toEqual(testValue); + }); + + it("should create a new file group on null value", () => { + const spy = spyOn(component, "createFileGroup"); + const testValue = null; + component.fileGroupPicked(testValue); + fixture.detectChanges(); + expect(spy).toHaveBeenCalledOnce(); + }); + + afterEach(() => { + fixture.destroy(); + }); +}); diff --git a/desktop/src/app/components/data/shared/file-group-picker/file-group-picker.component.ts b/desktop/src/app/components/data/shared/file-group-picker/file-group-picker.component.ts index 18d94e0b26..4deb5644f5 100644 --- a/desktop/src/app/components/data/shared/file-group-picker/file-group-picker.component.ts +++ b/desktop/src/app/components/data/shared/file-group-picker/file-group-picker.component.ts @@ -140,18 +140,25 @@ export class FileGroupPickerComponent implements ControlValueAccessor, OnInit, O return null; } - public createFileGroup(dropdownValue: string) { - if (!dropdownValue) { - const dialog = this.dialogService.open(FileGroupCreateFormComponent); - dialog.afterClosed().subscribe((activity?: Activity) => { - const newFileGroupName = dialog.componentInstance.getCurrentValue().name; - this.value.setValue(this.fileGroupService.addFileGroupPrefix(newFileGroupName)); - this.changeDetector.markForCheck(); - this._uploadActivity.next(activity); - }); + public fileGroupPicked(value: string) { + if (value) { + this.writeValue(value); + this.changeDetector.markForCheck(); + } else { + this.createFileGroup(); } } + public createFileGroup() { + const dialog = this.dialogService.open(FileGroupCreateFormComponent); + dialog.afterClosed().subscribe((activity?: Activity) => { + const newFileGroupName = dialog.componentInstance.getCurrentValue().name; + this.value.setValue(this.fileGroupService.addFileGroupPrefix(newFileGroupName)); + this.changeDetector.markForCheck(); + this._uploadActivity.next(activity); + }); + } + public trackFileGroup(_: number, fileGroup: BlobContainer) { return fileGroup.id; } diff --git a/desktop/src/app/components/data/shared/file-group-picker/file-group-picker.html b/desktop/src/app/components/data/shared/file-group-picker/file-group-picker.html index 0ecaa7c1e1..b3f7cce428 100644 --- a/desktop/src/app/components/data/shared/file-group-picker/file-group-picker.html +++ b/desktop/src/app/components/data/shared/file-group-picker/file-group-picker.html @@ -8,11 +8,12 @@ {{hint}} - + + [value]="fileGroup.name" + [label]="fileGroup.name"> diff --git a/desktop/src/app/components/gallery/submit/market-application.model.ts b/desktop/src/app/components/gallery/submit/market-application.model.ts index c0463c89b8..36c992405c 100644 --- a/desktop/src/app/components/gallery/submit/market-application.model.ts +++ b/desktop/src/app/components/gallery/submit/market-application.model.ts @@ -38,9 +38,12 @@ export class NcjParameterWrapper { } private _computeDefaultValue() { - if (this.param.defaultValue) { - this.defaultValue = this.param.defaultValue; + let defaultValue = this.param.defaultValue; + if (typeof defaultValue === "string" && + defaultValue.toLowerCase().trim() === "none") { + defaultValue = ""; } + this.defaultValue = this.param.defaultValue = defaultValue; } private _computeDescription() { diff --git a/desktop/src/app/components/gallery/submit/parameter-input/parameter-input.component.ts b/desktop/src/app/components/gallery/submit/parameter-input/parameter-input.component.ts index aae2ed559a..249a12676f 100644 --- a/desktop/src/app/components/gallery/submit/parameter-input/parameter-input.component.ts +++ b/desktop/src/app/components/gallery/submit/parameter-input/parameter-input.component.ts @@ -41,11 +41,10 @@ export class ParameterInputComponent implements ControlValueAccessor, OnChanges, this._subs.push(this.parameterValue.valueChanges.pipe( distinctUntilChanged(), ).subscribe((query: string) => { - if (this._propagateChange) { - this._propagateChange(query); - } - }), - ); + if (this._propagateChange) { + this._propagateChange(query); + } + })); } public ngOnChanges(changes) { diff --git a/desktop/src/app/components/gallery/submit/submit-ncj-template.component.ts b/desktop/src/app/components/gallery/submit/submit-ncj-template.component.ts index cdc22f72c5..74e6a1e237 100644 --- a/desktop/src/app/components/gallery/submit/submit-ncj-template.component.ts +++ b/desktop/src/app/components/gallery/submit/submit-ncj-template.component.ts @@ -347,7 +347,7 @@ export class SubmitNcjTemplateComponent implements OnInit, OnChanges, OnDestroy let validator = Validators.required; if (exists(template.parameters[key].defaultValue)) { defaultValue = String(template.parameters[key].defaultValue); - if (template.parameters[key].defaultValue === "") { + if (defaultValue.trim() === "") { validator = null; } } diff --git a/desktop/src/app/components/pool/action/add/container-configuration-picker/container-configuration-picker.component.spec.ts b/desktop/src/app/components/pool/action/add/container-configuration-picker/container-configuration-picker.component.spec.ts index 7a44c26d06..e3eb2420b4 100644 --- a/desktop/src/app/components/pool/action/add/container-configuration-picker/container-configuration-picker.component.spec.ts +++ b/desktop/src/app/components/pool/action/add/container-configuration-picker/container-configuration-picker.component.spec.ts @@ -6,7 +6,7 @@ import { I18nTestingModule } from "@batch-flask/core/testing"; import { SelectComponent, SelectModule } from "@batch-flask/ui"; import { FormModule } from "@batch-flask/ui/form"; import { ContainerConfigurationAttributes, ContainerType } from "app/models"; -import { ContainerConfigurationDto } from "app/models/dtos"; +import { ContainerConfigurationDto, ContainerRegistryDto } from "app/models/dtos"; import { ContainerConfigurationPickerComponent } from "./container-configuration-picker.component"; import { ContainerImagesPickerComponent } from "./images-picker/container-images-picker.component"; import { ContainerRegistryPickerComponent } from "./registry-picker/container-registry-picker.component"; @@ -97,7 +97,7 @@ describe("ContainerConfigurationPickerComponent", () => { type: ContainerType.DockerCompatible, containerImageNames: [], containerRegistries: [ - { username: "foo", password: "pass123!", registryServer: "https://bar.com" }, + { username: "foo", password: "pass123!", registryServer: "https://bar.com" } as ContainerRegistryDto, ], })); }); diff --git a/desktop/src/app/models/azure-batch/pool/pool.spec.ts b/desktop/src/app/models/azure-batch/pool/pool.spec.ts index b0d9c61140..2a08b7972e 100644 --- a/desktop/src/app/models/azure-batch/pool/pool.spec.ts +++ b/desktop/src/app/models/azure-batch/pool/pool.spec.ts @@ -1,4 +1,4 @@ -import { ContainerType } from "app/models"; +import { ContainerRegistry, ContainerType } from "app/models"; import { List } from "immutable"; import { Pool } from "./pool"; @@ -13,7 +13,7 @@ describe("Pool Model", () => { username: "abc", password: "foo", registryServer: "hub.docker.com", - }, + } as ContainerRegistry, ], }, }, diff --git a/desktop/src/app/models/blob-container.ts b/desktop/src/app/models/blob-container.ts index f8de3b3674..50d73cf614 100644 --- a/desktop/src/app/models/blob-container.ts +++ b/desktop/src/app/models/blob-container.ts @@ -15,7 +15,7 @@ export interface BlobContainerAttributes { /** * Class for displaying blob container information. */ -@Model() +@Model("BlobContainer") export class BlobContainer extends Record implements NavigableRecord { // container name @Prop() public id: string; diff --git a/desktop/src/app/services/storage/blob-storage-client-proxy.ts b/desktop/src/app/services/storage/blob-storage-client-proxy.ts index e3241f8dda..2496841154 100644 --- a/desktop/src/app/services/storage/blob-storage-client-proxy.ts +++ b/desktop/src/app/services/storage/blob-storage-client-proxy.ts @@ -173,8 +173,13 @@ export class BlobStorageClientProxy implements StorageClient { containerName: string, options?: blob.RequestOptions ): Promise { - return this.remote.send(storageIpc.getContainerProperties, + const result = await this.remote.send(storageIpc.getContainerProperties, { ...this.storageInfo, containerName, options }); + + // The container name is not returned by the API, but we add it here, + // since it's used by the UI model + result.data.name = containerName; + return result; } /** diff --git a/desktop/src/app/services/storage/models/storage-blob.ts b/desktop/src/app/services/storage/models/storage-blob.ts index 4042ac914e..0ef2d3cceb 100644 --- a/desktop/src/app/services/storage/models/storage-blob.ts +++ b/desktop/src/app/services/storage/models/storage-blob.ts @@ -1,7 +1,7 @@ -import { BlobUploadCommonResponse, ContainerGetPropertiesResponse, CommonOptions, ContainerItem } from "@azure/storage-blob"; +import { BlobUploadCommonResponse, CommonOptions, ContainerGetPropertiesResponse } from "@azure/storage-blob"; import { Model, Prop, Record } from "@batch-flask/core"; -import { SharedAccessPolicy } from "./shared-access-policy"; import { BlobContainer } from "app/models"; +import { SharedAccessPolicy } from "./shared-access-policy"; // Placeholder; we don't use any options to storage-blob API requests export type RequestOptions = Partial; @@ -111,7 +111,7 @@ export interface StorageBlobResult { continuationToken?: string; } -export type ListContainersResult = StorageBlobResult; +export type ListContainersResult = StorageBlobResult; export type GetBlobPropertiesResult = StorageBlobResult; export type ListBlobsResult = StorageBlobResult; export type GetContainerPropertiesResult = diff --git a/desktop/src/app/services/storage/storage-container.service.ts b/desktop/src/app/services/storage/storage-container.service.ts index 4323f19ab5..ebf7501b43 100644 --- a/desktop/src/app/services/storage/storage-container.service.ts +++ b/desktop/src/app/services/storage/storage-container.service.ts @@ -72,8 +72,11 @@ export class StorageContainerService { continuationToken, { maxResults: options && options.maxResults }); - response.data.map(x => x.storageAccountId = params.storageAccountId); - return response; + const containers = response.data.map((container: BlobContainer) => { + container.name = container.id; + return container; + }); + return { data: containers }; }, logIgnoreError: storageIgnoredErrors, });