diff --git a/package-lock.json b/package-lock.json index e23f4475..49da0a76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,16 @@ "version": "1.15.0", "dependencies": { "@angular/animations": "^17.1.1", - "@angular/cdk": "^17.1.1", + "@angular/cdk": "^17.3.6", "@angular/common": "^17.1.1", "@angular/compiler": "^17.1.1", - "@angular/core": "^17.1.1", - "@angular/forms": "^17.1.1", - "@angular/material": "^17.1.1", + "@angular/core": "^17.3.5", + "@angular/forms": "^17.3.6", + "@angular/material": "^17.3.6", "@angular/platform-browser": "^17.1.1", "@angular/platform-browser-dynamic": "^17.1.1", "@angular/platform-server": "^17.1.1", - "@angular/router": "^17.1.1", + "@angular/router": "^17.3.5", "@angular/service-worker": "^17.1.1", "@angular/youtube-player": "^17.1.1", "@aws-sdk/client-s3": "^3.499.0", @@ -999,9 +999,9 @@ } }, "node_modules/@angular/cdk": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.1.1.tgz", - "integrity": "sha512-Q5qC6VUyT7N/hj8eETdh0bkmBmsXm0JZikhXdBvcDUl8yPbhMPKQCkx4UJzBrZJg/+78XyI9FI/q8w/yQAJZJA==", + "version": "17.3.6", + "resolved": "https://sres.web.boeing.com/artifactory/api/npm/npm-releases/@angular/cdk/-/cdk-17.3.6.tgz", + "integrity": "sha512-7eKrC61/6pmMAxllU/vYKadZRF7x7GxUYpA5G70fNaQsIUUiZvxx/SJN9AuZEoPGAtF6atKlJD8QVmFoDzv/Lw==", "dependencies": { "tslib": "^2.3.0" }, @@ -1126,9 +1126,9 @@ } }, "node_modules/@angular/core": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.1.1.tgz", - "integrity": "sha512-JtNYM9eHr8eUSrGPq/kn0+/F+TSZ7EBWxZhM1ZndOlGu1gA4fGhrDid4ZXIHIs07DbM4NZjMn+LhRyx02YDsSA==", + "version": "17.3.5", + "resolved": "https://sres.web.boeing.com/artifactory/api/npm/npm-releases/@angular/core/-/core-17.3.5.tgz", + "integrity": "sha512-y6P27lcrKy3yMx/rtMuGsAnDyVEsS3BdyArTXcD0TOImVGHhVIaB0L95DUCam3ajTe2f2x39eozJZDh7QSpJaw==", "dependencies": { "tslib": "^2.3.0" }, @@ -1141,9 +1141,9 @@ } }, "node_modules/@angular/forms": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.1.1.tgz", - "integrity": "sha512-rqHVzaJDV8+VbnfC6mDgzX6ooa0X0hmnd+XfuOZaEJ7MtyOmqQ8qas2PAKXU7nMIImYXfYc4O4XWbSc1pRy1Hw==", + "version": "17.3.6", + "resolved": "https://sres.web.boeing.com/artifactory/api/npm/npm-releases/@angular/forms/-/forms-17.3.6.tgz", + "integrity": "sha512-WXxWhwvgRfYLNP2dB4Qe83tavEh2LnS4H0uoiecWHXijW2R9z8304X1vEyS1EtQK7o/s8fCVDVDjeY+hxLnCLw==", "dependencies": { "tslib": "^2.3.0" }, @@ -1151,9 +1151,9 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.1.1", - "@angular/core": "17.1.1", - "@angular/platform-browser": "17.1.1", + "@angular/common": "17.3.6", + "@angular/core": "17.3.6", + "@angular/platform-browser": "17.3.6", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -1167,9 +1167,9 @@ } }, "node_modules/@angular/material": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.1.1.tgz", - "integrity": "sha512-Ngh/4MY3MAPd4Fe2kb9W8j8Ix+hA9MVPvppYTlSsYzvlhV8YhOEaH2nuv9hJLrOiurlRWt7VlW13YkufK4VBgg==", + "version": "17.3.6", + "resolved": "https://sres.web.boeing.com/artifactory/api/npm/npm-releases/@angular/material/-/material-17.3.6.tgz", + "integrity": "sha512-sttN0JNvd2QvCCFIsxb5noiy7tgQdWrwvmrkJ+3KguHh5X84jDliA/d8N7Xgy2IBLnS/q/Hl9DdRCOiItWG1bw==", "dependencies": { "@material/animation": "15.0.0-canary.7f224ddd4.0", "@material/auto-init": "15.0.0-canary.7f224ddd4.0", @@ -1222,7 +1222,7 @@ }, "peerDependencies": { "@angular/animations": "^17.0.0 || ^18.0.0", - "@angular/cdk": "17.1.1", + "@angular/cdk": "17.3.6", "@angular/common": "^17.0.0 || ^18.0.0", "@angular/core": "^17.0.0 || ^18.0.0", "@angular/forms": "^17.0.0 || ^18.0.0", @@ -1288,9 +1288,9 @@ } }, "node_modules/@angular/router": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.1.1.tgz", - "integrity": "sha512-pPIRX0v8agij2dRSU25iwj9qFy0S25cztsy7bGfZ+M510jwRCqu1JsitqXtQ85XSv/bdFqiNiFU0UbwVFl+QiQ==", + "version": "17.3.5", + "resolved": "https://sres.web.boeing.com/artifactory/api/npm/npm-releases/@angular/router/-/router-17.3.5.tgz", + "integrity": "sha512-KsIIs3t9IpxsdMSrJDZzO5WgIWkVE6Ep5WWiSyPIgEfA+ndGpJLmyv0d/r1yKKlYUJxz7Hde55o4thgT2n2x/A==", "dependencies": { "tslib": "^2.3.0" }, @@ -1298,9 +1298,9 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.1.1", - "@angular/core": "17.1.1", - "@angular/platform-browser": "17.1.1", + "@angular/common": "17.3.5", + "@angular/core": "17.3.5", + "@angular/platform-browser": "17.3.5", "rxjs": "^6.5.3 || ^7.4.0" } }, diff --git a/package.json b/package.json index dd30651e..a31266c5 100644 --- a/package.json +++ b/package.json @@ -20,16 +20,16 @@ "private": true, "dependencies": { "@angular/animations": "^17.1.1", - "@angular/cdk": "^17.1.1", + "@angular/cdk": "^17.3.6", "@angular/common": "^17.1.1", "@angular/compiler": "^17.1.1", - "@angular/core": "^17.1.1", - "@angular/forms": "^17.1.1", - "@angular/material": "^17.1.1", + "@angular/core": "^17.3.5", + "@angular/forms": "^17.3.6", + "@angular/material": "^17.3.6", "@angular/platform-browser": "^17.1.1", "@angular/platform-browser-dynamic": "^17.1.1", "@angular/platform-server": "^17.1.1", - "@angular/router": "^17.1.1", + "@angular/router": "^17.3.5", "@angular/service-worker": "^17.1.1", "@angular/youtube-player": "^17.1.1", "@aws-sdk/client-s3": "^3.499.0", @@ -56,9 +56,9 @@ "date-fns": "^3.3.1", "diff": "^5.1.0", "dom-to-image": "^2.6.0", + "dompurify": "^3.0.8", "export-to-csv": "^1.2.2", "filesize": "^10.1.0", - "dompurify": "^3.0.8", "has-ansi": "^5.0.1", "hocon-parser": "^1.0.1", "localforage": "^1.10.0", diff --git a/src/app/features/experiments/experiments.module.ts b/src/app/features/experiments/experiments.module.ts index ccaa052b..2aff8e29 100755 --- a/src/app/features/experiments/experiments.module.ts +++ b/src/app/features/experiments/experiments.module.ts @@ -87,6 +87,7 @@ import {MatInputModule} from '@angular/material/input'; import {MatSelectModule} from '@angular/material/select'; import {HesitateDirective} from '@common/shared/ui-components/directives/hesitate.directive'; import {ShowTooltipIfEllipsisDirective} from '@common/shared/ui-components/indicators/tooltip/show-tooltip-if-ellipsis.directive'; +import {MatButtonModule} from '@angular/material/button'; @NgModule({ @@ -149,7 +150,8 @@ import {ShowTooltipIfEllipsisDirective} from '@common/shared/ui-components/indic MatInputModule, MatSelectModule, HesitateDirective, - ShowTooltipIfEllipsisDirective + ShowTooltipIfEllipsisDirective, + MatButtonModule ], declarations: [ ExperimentsComponent, diff --git a/src/app/webapp-common/core/actions/common-auth.actions.ts b/src/app/webapp-common/core/actions/common-auth.actions.ts index e680a6e9..e3f08aee 100755 --- a/src/app/webapp-common/core/actions/common-auth.actions.ts +++ b/src/app/webapp-common/core/actions/common-auth.actions.ts @@ -75,6 +75,7 @@ export const getSignedUrl = createAction( disableCache?: number; dprsUrl?: string | boolean; error?: boolean; + method?: string; }}>() ); export const setSignedUrl = createAction( diff --git a/src/app/webapp-common/experiments/dumb/experiment-info-header/experiment-info-header.component.html b/src/app/webapp-common/experiments/dumb/experiment-info-header/experiment-info-header.component.html index 3684fa80..8cb3bf39 100644 --- a/src/app/webapp-common/experiments/dumb/experiment-info-header/experiment-info-header.component.html +++ b/src/app/webapp-common/experiments/dumb/experiment-info-header/experiment-info-header.component.html @@ -22,6 +22,9 @@ *ngIf="(!minimized) && shared" > +
+ +
= this.dialog.open(UploadArtifactDialogComponent, { + data: { + uploadUrl: this.upUrl, + task: this.experiment.id, + title: 'Upload Artifacts', + iconClass: 'al-icon al-ico-upload al-color blue-300', + } + }); + } else { + this.store.dispatch(addMessage('info', 'Upload only available in \'Draft\' state.')); + } + } } diff --git a/src/app/webapp-common/settings/admin/base-admin.service.ts b/src/app/webapp-common/settings/admin/base-admin.service.ts index 5b90b632..89d6dea6 100755 --- a/src/app/webapp-common/settings/admin/base-admin.service.ts +++ b/src/app/webapp-common/settings/admin/base-admin.service.ts @@ -3,7 +3,7 @@ import {Store} from '@ngrx/store'; import {from, fromEvent, Observable, of, Subject} from 'rxjs'; import {fromFetch} from 'rxjs/fetch'; import {catchError, debounceTime, filter, map, skip} from 'rxjs/operators'; -import {DeleteObjectsCommand, GetObjectCommand, ObjectIdentifier, S3Client, S3ClientConfig} from '@aws-sdk/client-s3'; +import {DeleteObjectsCommand, GetObjectCommand, ObjectIdentifier, S3Client, S3ClientConfig, PutObjectCommand} from '@aws-sdk/client-s3'; import {getSignedUrl} from '@aws-sdk/s3-request-presigner'; import {convertToReverseProxy, isFileserverUrl} from '~/shared/utils/url'; import { @@ -63,9 +63,9 @@ export class BaseAdminService { this.store.dispatch(showLocalFilePopUp({url})); } - signUrlIfNeeded(url: string, config?: { skipLocalFile?: boolean; skipFileServer?: boolean; disableCache?: number }, previousSignedUrl?: { signed: string; expires: number }): - Observable { - config = {...{skipLocalFile: true, skipFileServer: this.confService.getStaticEnvironment().production, disableCache: null}, ...config}; + signUrlIfNeeded(url: string, config?: { skipLocalFile?: boolean; skipFileServer?: boolean; disableCache?: number; method?: string; }, + previousSignedUrl?: { signed: string; expires: number }): Observable { + config = {...{skipLocalFile: true, skipFileServer: this.confService.getStaticEnvironment().production, disableCache: null, method: 'GET'}, ...config}; if (isFileserverUrl(url)) { if (this.environment.communityServer) { @@ -97,7 +97,7 @@ export class BaseAdminService { return of({type: 'popup', bucket: bucketKeyEndpoint, provider: 'azure'}); } const s3 = this.findOrInitBucketS3(bucketKeyEndpoint); - if (s3) { + if (s3 && (config.method === 'GET' || config.method === '')) { // eslint-disable-next-line @typescript-eslint/naming-convention const command = new GetObjectCommand({ Key: bucketKeyEndpoint.Key, @@ -109,6 +109,18 @@ export class BaseAdminService { unsignableHeaders: new Set(['x-amz-content-sha256', 'x-id']), })) .pipe(map(signed => ({type: 'sign', signed, expires: (new Date()).getTime() + FOUR_DAYS * 1000}))); + } else if(s3 && config.method === 'PUT') { + // eslint-disable-next-line @typescript-eslint/naming-convention + const command = new PutObjectCommand({ + Key: bucketKeyEndpoint.Key, + Bucket: bucketKeyEndpoint.Bucket/*, ResponseContentType: 'image/jpeg'*/ + }); + return from(getSignedUrl(s3, command, { + expiresIn: FOUR_DAYS, + unhoistableHeaders: new Set(['x-amz-content-sha256', 'x-id']), + unsignableHeaders: new Set(['x-amz-content-sha256', 'x-id']) + })) + .pipe(map(signed => ({type: 'sign', signed, expires: (new Date()).getTime() + FOUR_DAYS * 1000}))); } else if (isGoogleCloudUrl(url) && !previousSignedUrl?.signed) { return of({type: 'sign', signed: this.signGoogleCloudUrl(url), expires: Number.MAX_VALUE}); } else { diff --git a/src/app/webapp-common/shared/ui-components/overlay/upload-artifact-dialog/upload-artifact-dialog.component.html b/src/app/webapp-common/shared/ui-components/overlay/upload-artifact-dialog/upload-artifact-dialog.component.html new file mode 100644 index 00000000..a1eeb908 --- /dev/null +++ b/src/app/webapp-common/shared/ui-components/overlay/upload-artifact-dialog/upload-artifact-dialog.component.html @@ -0,0 +1,43 @@ + +
+
+
+ + Artifact(s) + + + File selection is required. + +
+
+ Artifact name(s) + + + +
+ + Artifact Type + + {{type.viewValue}} + + Type selection is required. + + + Bucket URL + + + + Artifact Mode + Output + Input + +
+
*NOTE* These settings apply to ALL FILES selected for upload.
+ +
+ + +
+
diff --git a/src/app/webapp-common/shared/ui-components/overlay/upload-artifact-dialog/upload-artifact-dialog.component.scss b/src/app/webapp-common/shared/ui-components/overlay/upload-artifact-dialog/upload-artifact-dialog.component.scss new file mode 100644 index 00000000..f72f0c92 --- /dev/null +++ b/src/app/webapp-common/shared/ui-components/overlay/upload-artifact-dialog/upload-artifact-dialog.component.scss @@ -0,0 +1,46 @@ +.body { + margin-bottom: 3rem !important; + } + + .buttons { + display: inline-block; + left: 50%; + position: relative; + transform: translateX(-50%); + margin-top: 41px; + + button { + text-transform: uppercase; + min-width: 126px; + } + } + + #fileInput { + position: absolute; + cursor: pointer; + z-index: 10; + opacity: 0; + height: 100%; + width: 100%; + left: 0px; + top: 0px; + } + + .mat-toolbar-single-row { + height: auto !important; + background: transparent; + padding: 0; + } + + .mat-toolbar-single-row button { + width: 100px; + } + + .mat-form-field { + width: 100%; + } + + .option { + color: #000; + } + \ No newline at end of file diff --git a/src/app/webapp-common/shared/ui-components/overlay/upload-artifact-dialog/upload-artifact-dialog.component.ts b/src/app/webapp-common/shared/ui-components/overlay/upload-artifact-dialog/upload-artifact-dialog.component.ts new file mode 100644 index 00000000..c9a0b455 --- /dev/null +++ b/src/app/webapp-common/shared/ui-components/overlay/upload-artifact-dialog/upload-artifact-dialog.component.ts @@ -0,0 +1,265 @@ +import { Component, Inject, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { addMessage } from '@common/core/actions/layout.actions'; +import { UploadArtifactDialogConfig, ArtifactType } from './upload-artifact-dialog.model'; +import { ApiTasksService } from '~/business-logic/api-services/tasks.service'; +import { Artifact } from '~/business-logic/model/tasks/artifact'; +import { IExecutionForm } from '~/features/experiments/shared/experiment-execution.model'; +import { Sha256 } from '@aws-crypto/sha256-browser'; +import { HttpClient } from '@angular/common/http'; +import { Observable, Subscription, filter, map, take } from 'rxjs'; +import { selectSignedUrl } from '@common/core/reducers/common-auth-reducer'; +import { getSignedUrl } from '@common/core/actions/common-auth.actions'; +import { selectExperimentExecutionInfoData } from '@common/experiments/reducers'; +import { ArtifactModeEnum } from '~/business-logic/model/tasks/artifactModeEnum'; +import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'; + +@Component({ + selector: 'sm-upload-artifact-dialog', + templateUrl: './upload-artifact-dialog.component.html', + styleUrls: ['./upload-artifact-dialog.component.scss'] +}) +export class UploadArtifactDialogComponent implements OnInit, OnDestroy { + artifactType: ArtifactType[] = [ + {value: 'input-model', viewValue: 'Input Model'}, + {value: 'output-model', viewValue: 'Output Model'}, + {value: 'data-audit', viewValue: 'Data Audit'}, + {value: 'other', viewValue: 'Other'}, + ]; + uploadForm = new FormGroup({ + fName: new FormControl({value: '', disabled: false}, Validators.required), + fInput: new FormControl(''), + artType: new FormControl({value: this.artifactType[3].value, disabled: true}), + upDest: new FormControl({value: '', disabled: false}), + mode: new FormControl({value: 'output', disabled: false}) + }); + + @Input() displayX: boolean = true; + title: string; + body?: string; + task?: string; + currentFileList?: Array; + currentFile: File; + template?: TemplateRef; + iconClass = ''; + iconData = ''; + centerText: boolean; + message: string; + uploadUrl: string; + hashList: Array; + upCount = 0; + filesSelected: boolean = false; + upKeys: Array; + + public executionInfo$: Observable; + public executionData: IExecutionForm; + private executionDataSubscription: Subscription; + + constructor( + @Inject(MAT_DIALOG_DATA) public data: UploadArtifactDialogConfig, + public dialogRef: MatDialogRef, + private store: Store, + private dialog: MatDialog, + private tasksApi: ApiTasksService, + private http: HttpClient, + ) { + this.executionInfo$ = this.store.select(selectExperimentExecutionInfoData); + this.title = data.title || ''; + this.body = data.body || ''; + this.task = data.task || ''; + this.template = data.template; + this.iconClass = data.iconClass || ''; + this.iconData = data.iconData || ''; + this.centerText = data.centerText ?? false; + } + + ngOnInit() { + this.executionDataSubscription = this.executionInfo$.subscribe(formData => { + this.executionData = formData; + }); + this.uploadUrl = this.executionData.output.destination; + this.uploadForm.controls['upDest'].setValue(this.uploadUrl); + } + + ngOnDestroy(): void { + this.executionDataSubscription.unsubscribe(); + } + + closeDialog() { + this.dialogRef.close(); + } + + selectFiles(event: any) { + let fileName = ''; + this.currentFileList = []; + this.upKeys = []; + if (event.target.files && event.target.files.length > 0) { + this.filesSelected = true; + for(const index in event.target.files) { + let idx = Number(index); + this.currentFileList[idx] = event.target.files[idx]; + } + const finalIndex = (this.currentFileList.length - 1); + for(const [index, element] of this.currentFileList.entries()){ + this.upKeys.push(element.name.substring(0, element.name.lastIndexOf('.'))); + fileName += element.name; + if(finalIndex >= 1 && index !== finalIndex){ + fileName += ', '; + } + } + this.createHash(this.currentFileList); + this.uploadForm.controls['fName'].setValue(fileName); + } else { + this.uploadForm.controls['fName'].setValue(''); + this.uploadForm.controls['fName'].setErrors({required: true}); + } + } + + updateName(index: number, event: any) { + this.upKeys[index] = event.target.value; + } + + async uploadFiles() { + let upUrl = this.uploadForm.controls['upDest'].value; + let signedUrl = ''; + + if(this.currentFileList && upUrl) { + if(!upUrl.endsWith('/')) { + upUrl += '/uploaded-artifacts/'; + } else { + upUrl += 'uploaded-artifacts/'; + } + this.uploadUrl = upUrl; + for(const file of this.currentFileList) { + const urlToSign = this.uploadUrl + file.name; + this.signUrl(urlToSign).subscribe({ + next: (res) => { + signedUrl = res; + this.upload(signedUrl, file); + }, + error: (err) => { + this.store.dispatch(addMessage('error', `${file.name} upload failed: ${err.error?.meta?.result_msg}`)); + throw new Error(`upload failed - sign url ${err.error?.meta?.result_msg}`); + } + }); + } + } else if(this.currentFileList) { + const confirmDialogRef: MatDialogRef = this.dialog.open(ConfirmDialogComponent, { + data: { + title: 'Missing S3 Bucket', + body: 'Continue without S3 bucket storage?', + yes: 'Continue', + no: 'Cancel', + iconClass: 'al-icon al-ico-question-mark al-color blue-300', + } + }); + + confirmDialogRef.afterClosed().pipe(take(1)).subscribe((confirmed) => { + if (confirmed) { + this.updateArtifact(this.currentFileList); + } + }); + } else { + this.store.dispatch(addMessage('error', 'No files selected.')); + } + } + + protected signUrl(url: string) { + this.store.dispatch(getSignedUrl({url, config: {method: "PUT"}})); + return this.store.select(selectSignedUrl(url)) + .pipe( + map((res => res?.signed)), + filter(signedUrl => !!signedUrl), + take(1) + ); + } + + protected upload(url: string, file: File) { + let error = false; + if(url && file) { + this.http.request("PUT", url, { + body: file, + reportProgress: true, + observe: 'events', + }).subscribe({ + next: () => {}, + error: (err) => { + this.store.dispatch(addMessage('error', `${file.name} upload to bucket failed with status: ${err.status}\nError body: ${err.error}\nCheck bucket credentials.`)); + throw new Error(`upload failed - http request ${err.error}`); + }, + complete: () => { + this.upCount++; + if(this.upCount === this.currentFileList.length) { + this.updateArtifact(this.currentFileList); + } + } + }); + } + } + + updateArtifact(fileList: Array) { + let updateList: Array = []; + let fileHash = ''; + let artName = ''; + for(const [index, item] of fileList.entries()) { + const ts = (new Date().getTime())/1000.0; // Convert to seconds so the Python SDK can read it + artName = this.upKeys[index]; + for(const hash of this.hashList) { + if(item.name === hash.fileName) { + fileHash = hash.fileHashHex; + } + } + updateList.push({ + key: artName, + type: this.uploadForm.controls['artType'].value, + content_size: item.size, + timestamp: ts, + hash: 'SHA256: ' + fileHash, + uri: this.uploadUrl + item.name, + mode: this.uploadForm.controls['mode'].value as ArtifactModeEnum + }); + } + + this.tasksApi.tasksAddOrUpdateArtifacts({ + task: this.task, + artifacts: updateList, + }, null, 'body', true).subscribe({ + next: () => {}, + error: err => this.store.dispatch(addMessage('error', `Error ${err.error?.meta?.result_msg}`)), + complete: () => { + this.store.dispatch(addMessage('success', 'Artifact(s) uploaded successfully.')); + this.upCount = 0; + this.closeDialog(); + } + }); + } + + async createHash(fileList: File[]) { + this.hashList = []; + let fileBuff: ArrayBuffer; + let fileHashSha256: any; + let fileHashHex: any; + const hash = new Sha256(); + for(const element of fileList){ + element.arrayBuffer().then(async buff => { + let fileName = element.name; + fileBuff = buff; + hash.update(fileBuff); + fileHashSha256 = await hash.digest(); + fileHashHex = this.createHexStr(fileHashSha256); + this.hashList.push({fileName, fileHashHex}); + }); + } + } + + createHexStr(arr: Uint8Array) { + let result = ''; + for (const element of arr) { + result += element.toString(16); + } + return result.toString(); + } + +} diff --git a/src/app/webapp-common/shared/ui-components/overlay/upload-artifact-dialog/upload-artifact-dialog.model.ts b/src/app/webapp-common/shared/ui-components/overlay/upload-artifact-dialog/upload-artifact-dialog.model.ts new file mode 100644 index 00000000..10fd676e --- /dev/null +++ b/src/app/webapp-common/shared/ui-components/overlay/upload-artifact-dialog/upload-artifact-dialog.model.ts @@ -0,0 +1,18 @@ +import {TemplateRef} from '@angular/core'; + +export interface UploadArtifactDialogConfig { + width?: number; + centerText?: boolean; + title?: string; + body?: string; + task?: string; + fileName?: string; + template?: TemplateRef; + iconClass?: string; // the icon class (see icons.scss). + iconData?: string; // the icon class (see icons.scss). +} + +export interface ArtifactType { + value: string; + viewValue: string; +}