mirror of
https://github.com/clearml/clearml-web
synced 2025-03-13 07:08:17 +00:00
Merge a982ad6c45
into d4f9424589
This commit is contained in:
commit
ccf15eb760
54
package-lock.json
generated
54
package-lock.json
generated
@ -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"
|
||||
}
|
||||
},
|
||||
|
12
package.json
12
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",
|
||||
|
@ -27,6 +27,11 @@ import {MAT_TOOLTIP_DEFAULT_OPTIONS, MatTooltipDefaultOptions} from '@angular/ma
|
||||
import {UpdateNotifierComponent} from '@common/shared/ui-components/overlay/update-notifier/update-notifier.component';
|
||||
import {ChooseColorModule} from '@common/shared/ui-components/directives/choose-color/choose-color.module';
|
||||
import {SpinnerComponent} from '@common/shared/ui-components/overlay/spinner/spinner.component';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { UploadArtifactDialogComponent } from '@common/shared/ui-components/overlay/upload-artifact-dialog/upload-artifact-dialog.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations : [AppComponent],
|
||||
@ -61,6 +66,10 @@ import {SpinnerComponent} from '@common/shared/ui-components/overlay/spinner/spi
|
||||
UpdateNotifierComponent,
|
||||
ChooseColorModule,
|
||||
SpinnerComponent,
|
||||
MatButtonModule,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
UploadArtifactDialogComponent
|
||||
],
|
||||
providers : [
|
||||
UserPreferences,
|
||||
|
@ -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,
|
||||
|
@ -75,6 +75,7 @@ export const getSignedUrl = createAction(
|
||||
disableCache?: number;
|
||||
dprsUrl?: string | boolean;
|
||||
error?: boolean;
|
||||
method?: string;
|
||||
}}>()
|
||||
);
|
||||
export const setSignedUrl = createAction(
|
||||
|
@ -18,6 +18,7 @@
|
||||
</sm-labeled-row>
|
||||
<sm-labeled-row label="FILE SIZE" data-id="fileSizeId">{{(artifact?.content_size | filesize : fileSizeConfigStorage) || ''}}</sm-labeled-row>
|
||||
<sm-labeled-row label="HASH" data-id="hashId">{{artifact?.hash}}</sm-labeled-row>
|
||||
<sm-labeled-row label="MODE" data-id="modeId">{{artifact?.mode | titlecase}}</sm-labeled-row>
|
||||
<sm-labeled-row *ngFor="let data of artifact?.display_data" [label]="data[0]| uppercase">{{data[1]}}</sm-labeled-row>
|
||||
</div>
|
||||
</sm-editable-section>
|
||||
@ -32,5 +33,5 @@
|
||||
[formData]="artifact?.type_data?.preview"
|
||||
></sm-scroll-textarea>
|
||||
</sm-editable-section>
|
||||
|
||||
<button *ngIf="enableDeleteButton()" class="btn btn-neon" style="margin: auto;" (click)="deleteArtifact(artifact?.key, artifact?.mode)">DELETE ARTIFACT</button>
|
||||
|
||||
|
@ -1,12 +1,27 @@
|
||||
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, Input, Inject} from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
import {Artifact} from '~/business-logic/model/tasks/artifact';
|
||||
import {BaseClickableArtifactComponent} from '../base-clickable-artifact.component';
|
||||
import {fileSizeConfigStorage} from '@common/shared/pipes/filesize.pipe';
|
||||
import { ApiTasksService } from '~/business-logic/api-services/tasks.service';
|
||||
import { ConfirmDialogComponent } from '@common/shared/ui-components/overlay/confirm-dialog/confirm-dialog.component';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { addMessage } from '@common/core/actions/layout.actions';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { IExperimentInfo } from '~/features/experiments/shared/experiment-info.model';
|
||||
import { selectExperimentInfoData } from '~/features/experiments/reducers';
|
||||
import { ArtifactId } from '~/business-logic/model/tasks/artifactId';
|
||||
import { deleteS3Sources } from '@common/shared/entity-page/entity-delete/common-delete-dialog.actions';
|
||||
import { EXPERIMENTS_STATUS_LABELS } from '~/features/experiments/shared/experiments.const';
|
||||
|
||||
@Component({
|
||||
selector: 'sm-experiment-artifact-item-view',
|
||||
templateUrl: './experiment-artifact-item-view.component.html',
|
||||
styleUrls: ['./experiment-artifact-item-view.component.scss'],
|
||||
providers: [
|
||||
{ provide: MAT_DIALOG_DATA, useValue: {} },
|
||||
{ provide: MatDialogRef, useValue: {} }
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ExperimentArtifactItemViewComponent extends BaseClickableArtifactComponent{
|
||||
@ -14,11 +29,28 @@ export class ExperimentArtifactItemViewComponent extends BaseClickableArtifactCo
|
||||
public isLinkable: boolean;
|
||||
public fileSizeConfigStorage = fileSizeConfigStorage;
|
||||
public inMemorySize: boolean;
|
||||
public experimentInfo$: Observable<IExperimentInfo>;
|
||||
private _artifact: Artifact;
|
||||
private artifactId: ArtifactId;
|
||||
private _experiment: any;
|
||||
|
||||
@Input() editable: boolean;
|
||||
@Input() downloading: boolean;
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: ConfirmDialogComponent,
|
||||
public dialogRef: MatDialogRef<ConfirmDialogComponent>,
|
||||
private dialog: MatDialog,
|
||||
private tasksApi: ApiTasksService,
|
||||
){
|
||||
super();
|
||||
this.experimentInfo$ = this.store.select(selectExperimentInfoData);
|
||||
this.artifactId = {key: '', mode: 'output'};
|
||||
this.experimentInfo$.subscribe((res) => {
|
||||
this._experiment = res;
|
||||
});
|
||||
}
|
||||
|
||||
@Input() set artifact(artifact: Artifact) {
|
||||
this._artifact = artifact;
|
||||
if(artifact){
|
||||
@ -39,6 +71,14 @@ export class ExperimentArtifactItemViewComponent extends BaseClickableArtifactCo
|
||||
return this._artifact;
|
||||
}
|
||||
|
||||
getStatusLabel() {
|
||||
return EXPERIMENTS_STATUS_LABELS[this._experiment?.status] || '';
|
||||
}
|
||||
|
||||
enableDeleteButton() {
|
||||
return this.getStatusLabel() === 'Draft';
|
||||
}
|
||||
|
||||
linkClicked(event: Event) {
|
||||
this.signUrl(this.artifact.uri).subscribe(signed => {
|
||||
const a = document.createElement('a');
|
||||
@ -48,4 +88,34 @@ export class ExperimentArtifactItemViewComponent extends BaseClickableArtifactCo
|
||||
});
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
deleteArtifact(key, mode) {
|
||||
this.artifactId = {key: key, mode: mode};
|
||||
const confirmDialogRef: MatDialogRef<any, boolean> = this.dialog.open(ConfirmDialogComponent, {
|
||||
data: {
|
||||
title: 'Delete Artifact',
|
||||
body: 'Are you sure you want to delete artifact ' + key + ' from the experiment and S3?<br /><strong>This cannot be undone.</strong>',
|
||||
yes: 'Delete',
|
||||
no: 'Cancel',
|
||||
iconClass: 'al-icon al-ico-trash al-color blue-300',
|
||||
}
|
||||
});
|
||||
|
||||
confirmDialogRef.afterClosed().pipe(take(1)).subscribe((confirmed) => {
|
||||
if (confirmed) {
|
||||
this.tasksApi.tasksDeleteArtifacts({
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
task: this._experiment.id,
|
||||
artifacts: [this.artifactId]
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
}, null, 'body', true).subscribe({
|
||||
next: () => {
|
||||
this.store.dispatch(deleteS3Sources({files: [this.artifact.uri]}))
|
||||
},
|
||||
error: err => this.store.dispatch(addMessage('error', `Error ${err.error?.meta?.result_msg}`)),
|
||||
complete: () => this.store.dispatch(addMessage('success', 'Artifact deleted successfully.')),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,9 @@
|
||||
*ngIf="(!minimized) && shared"
|
||||
><i class="al-icon al-ico-link sm-md"></i></div>
|
||||
<sm-id-badge class="me-3" [id]="experiment?.id" (copied)="copyToClipboard()" *ngIf="!viewId"></sm-id-badge>
|
||||
<div class="line-item pointer" smTooltip="Upload Artifacts">
|
||||
<i class="al-icon al-ico-upload line-item pointer" (click)="openUploadDialog()"></i>
|
||||
</div>
|
||||
<span class="comment line-item"
|
||||
[delay]="1000" [action]="'leave'"
|
||||
(smHesitate)="menuHesitate.hesitateStatus && menu.closed.emit()"
|
||||
|
@ -26,6 +26,8 @@ import {
|
||||
import {addMessage} from '@common/core/actions/layout.actions';
|
||||
import {MatMenuTrigger} from '@angular/material/menu';
|
||||
import { selectExperimentsTags } from '@common/experiments/reducers';
|
||||
import { UploadArtifactDialogComponent } from '@common/shared/ui-components/overlay/upload-artifact-dialog/upload-artifact-dialog.component';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'sm-experiment-info-header',
|
||||
@ -43,6 +45,7 @@ export class ExperimentInfoHeaderComponent implements OnDestroy {
|
||||
public shared: boolean;
|
||||
public isPipeline: boolean;
|
||||
public selectedDisableAvailable = {};
|
||||
private upUrl: string;
|
||||
|
||||
@Input() editable: boolean = true;
|
||||
@Input() infoData;
|
||||
@ -57,7 +60,7 @@ export class ExperimentInfoHeaderComponent implements OnDestroy {
|
||||
@ViewChild('tagsMenuTrigger') tagMenuTrigger: MatMenuTrigger;
|
||||
@ViewChild(TagsMenuComponent) tagMenu: TagsMenuComponent;
|
||||
|
||||
constructor(private store: Store, private router: Router, private activatedRoute: ActivatedRoute) {
|
||||
constructor(private store: Store, private router: Router, private activatedRoute: ActivatedRoute, private dialog: MatDialog) {
|
||||
this.tagsFilterByProject$ = this.store.select(selectTagsFilterByProject);
|
||||
this.projectTags$ = this.store.select(selectExperimentsTags);
|
||||
this.companyTags$ = this.store.select(selectCompanyTags);
|
||||
@ -152,4 +155,23 @@ export class ExperimentInfoHeaderComponent implements OnDestroy {
|
||||
copyToClipboard() {
|
||||
this.store.dispatch(addMessage('success', 'Copied to clipboard'));
|
||||
}
|
||||
|
||||
enableUploadButton() {
|
||||
return this.getStatusLabel() === 'Draft';
|
||||
}
|
||||
|
||||
public openUploadDialog() {
|
||||
if(this.enableUploadButton()) {
|
||||
const uploadDialogRef: MatDialogRef<any> = 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.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<SignResponse> {
|
||||
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<SignResponse> {
|
||||
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 {
|
||||
@ -230,7 +242,7 @@ export class BaseAdminService {
|
||||
Bucket: bucketKeyEndpoint.Bucket,
|
||||
Delete: {
|
||||
Quiet: true,
|
||||
Objects: files.map(file => ({Key: file} as ObjectIdentifier))
|
||||
Objects: files.map(() => ({Key: bucketKeyEndpoint.Key} as ObjectIdentifier))
|
||||
}
|
||||
});
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
@ -0,0 +1,43 @@
|
||||
<sm-dialog-template [displayX]="displayX && !dialogRef.disableClose" [header]="title"
|
||||
(xClicked)="closeDialog()" [closeOnX]="false" [iconClass]="iconClass"
|
||||
[iconData]="iconData">
|
||||
<div *ngIf="body" class="body" [class.text-center]="this.centerText" [innerHTML]="body | safe: 'html'"></div>
|
||||
<form [formGroup]="uploadForm">
|
||||
<div>
|
||||
<mat-form-field class="mat-mdc-form-field w-100 mat-light mat-primary" appearance="outline">
|
||||
<mat-label>Artifact(s)</mat-label>
|
||||
<input matInput name="fileName" formControlName="fName" placeholder="Select File(s)" required readonly/>
|
||||
<input type="file" id="fileInput" (change)="selectFiles($event)" formControlName="fInput" name="fileInput" multiple/>
|
||||
<mat-error *ngIf="uploadForm.get('fName').hasError('required')">File selection is required.</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngIf="this.filesSelected">
|
||||
<mat-label>Artifact name(s)</mat-label>
|
||||
<mat-form-field *ngFor="let name of this.upKeys; let i = index" class="mat-mdc-form-field w-100 mat-light mat-primary" appearance="outline">
|
||||
<input matInput value={{name}} (change)="updateName(i, $event)"/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<mat-form-field class="mat-mdc-form-field w-100 mat-light mat-primary" appearance="outline">
|
||||
<mat-label>Artifact Type</mat-label>
|
||||
<mat-select formControlName="artType" name="artType" required>
|
||||
<mat-option *ngFor="let type of artifactType" [value]="type.value"><span class="option">{{type.viewValue}}</span></mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="uploadForm.get('artType').hasError('required')">Type selection is required.</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-mdc-form-field w-100 mat-light mat-primary" appearance="outline">
|
||||
<mat-label>Bucket URL</mat-label>
|
||||
<input matInput name="bucketUrl" formControlName="upDest" placeholder="s3://bucket/folder" />
|
||||
</mat-form-field>
|
||||
<mat-radio-group formControlName="mode">
|
||||
<mat-label>Artifact Mode</mat-label>
|
||||
<mat-radio-button value="output">Output</mat-radio-button>
|
||||
<mat-radio-button value="input">Input</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</form>
|
||||
<div><strong>*NOTE*</strong> These settings apply to <strong><u>ALL FILES</u></strong> selected for upload.</div>
|
||||
|
||||
<div class="buttons">
|
||||
<button (click)="closeDialog()" class="btn btn-outline-neon">CANCEL</button>
|
||||
<button [disabled]="uploadForm.invalid" (click)="uploadFiles()" class="btn btn-neon">UPLOAD</button>
|
||||
</div>
|
||||
</sm-dialog-template>
|
@ -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;
|
||||
}
|
||||
|
@ -0,0 +1,284 @@
|
||||
import { Component, Inject, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
import { FormsModule, ReactiveFormsModule, 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';
|
||||
import { DialogTemplateComponent } from '@common/shared/ui-components/overlay/dialog-template/dialog-template.component';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { SafePipe } from '@common/shared/pipes/safe.pipe';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
|
||||
@Component({
|
||||
selector: 'sm-upload-artifact-dialog',
|
||||
templateUrl: './upload-artifact-dialog.component.html',
|
||||
styleUrls: ['./upload-artifact-dialog.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
SafePipe,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
DialogTemplateComponent,
|
||||
CommonModule,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
MatRadioModule,
|
||||
]
|
||||
})
|
||||
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<File>;
|
||||
currentFile: File;
|
||||
template?: TemplateRef<any>;
|
||||
iconClass = '';
|
||||
iconData = '';
|
||||
centerText: boolean;
|
||||
message: string;
|
||||
uploadUrl: string;
|
||||
hashList: Array<any>;
|
||||
upCount = 0;
|
||||
filesSelected: boolean = false;
|
||||
upKeys: Array<string>;
|
||||
|
||||
public executionInfo$: Observable<IExecutionForm>;
|
||||
public executionData: IExecutionForm;
|
||||
private executionDataSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: UploadArtifactDialogConfig,
|
||||
public dialogRef: MatDialogRef<UploadArtifactDialogComponent>,
|
||||
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<any, boolean> = 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<File>) {
|
||||
let updateList: Array<Artifact> = [];
|
||||
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: 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();
|
||||
}
|
||||
|
||||
}
|
@ -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<any>;
|
||||
iconClass?: string; // the icon class (see icons.scss).
|
||||
iconData?: string; // the icon class (see icons.scss).
|
||||
}
|
||||
|
||||
export interface ArtifactType {
|
||||
value: string;
|
||||
viewValue: string;
|
||||
}
|
Loading…
Reference in New Issue
Block a user