diff --git a/package-lock.json b/package-lock.json index eb4b0c611..fc4c4f009 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@angular/platform-browser-dynamic": "^17.3.0", "@angular/router": "^17.3.0", "@egjs/hammerjs": "^2.0.17", - "@geoengine/openapi-client": "0.0.10", + "@geoengine/openapi-client": "https://gitpkg.vercel.app/glombiewski/openapi-client/typescript?fair-dataset-deletion", "codemirror": "~5.65.16", "d3": "~7.9.0", "dagre": "~0.8.5", @@ -3491,8 +3491,8 @@ }, "node_modules/@geoengine/openapi-client": { "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@geoengine/openapi-client/-/openapi-client-0.0.10.tgz", - "integrity": "sha512-xo9k2zlsrZfrOCSGEP7SiGoQ8UK3EQPA886L2lLnqdPHCqYBcbSbTGdhERsj3sHFfnqvVO8vMjNcW585js2PLQ==" + "resolved": "https://gitpkg.vercel.app/glombiewski/openapi-client/typescript?fair-dataset-deletion", + "integrity": "sha512-SDnGLejlH6PiecWnnwBuXGltqqU93/AyggyptqHUliVVubPm87NDKL7i9JD1cRpYX0JuLaoSorTDMNOSS7OeLA==" }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", @@ -24011,9 +24011,8 @@ "dev": true }, "@geoengine/openapi-client": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@geoengine/openapi-client/-/openapi-client-0.0.10.tgz", - "integrity": "sha512-xo9k2zlsrZfrOCSGEP7SiGoQ8UK3EQPA886L2lLnqdPHCqYBcbSbTGdhERsj3sHFfnqvVO8vMjNcW585js2PLQ==" + "version": "https://gitpkg.vercel.app/glombiewski/openapi-client/typescript?fair-dataset-deletion", + "integrity": "sha512-SDnGLejlH6PiecWnnwBuXGltqqU93/AyggyptqHUliVVubPm87NDKL7i9JD1cRpYX0JuLaoSorTDMNOSS7OeLA==" }, "@humanwhocodes/config-array": { "version": "0.11.14", diff --git a/package.json b/package.json index 290de086a..f51220676 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@angular/platform-browser-dynamic": "^17.3.0", "@angular/router": "^17.3.0", "@egjs/hammerjs": "^2.0.17", - "@geoengine/openapi-client": "0.0.10", + "@geoengine/openapi-client": "https://gitpkg.vercel.app/glombiewski/openapi-client/typescript?fair-dataset-deletion", "codemirror": "~5.65.16", "d3": "~7.9.0", "dagre": "~0.8.5", diff --git a/projects/common/src/lib/datasets/datasets.service.ts b/projects/common/src/lib/datasets/datasets.service.ts index c347382ad..57db744ca 100644 --- a/projects/common/src/lib/datasets/datasets.service.ts +++ b/projects/common/src/lib/datasets/datasets.service.ts @@ -2,9 +2,11 @@ import {Injectable} from '@angular/core'; import { DataPath, Dataset, + DatasetAccessStatusResponse, DatasetDefinition, DatasetListing, DatasetsApi, + ExpirationChange, MetaDataDefinition, MetaDataSuggestion, OrderBy, @@ -123,4 +125,19 @@ export class DatasetsService { return datasetApi.listVolumesHandler(); } + + async getDatasetStatus(datasetName: string): Promise { + const datasetApi = await firstValueFrom(this.datasetApi); + + return datasetApi.getDatasetStatus({dataset: datasetName}); + } + + async expireDataset(datasetName: string, expirationChange: ExpirationChange): Promise { + const datasetApi = await firstValueFrom(this.datasetApi); + + return datasetApi.setDatasetExpiration({ + dataset: datasetName, + expirationChange: expirationChange, + }); + } } diff --git a/projects/common/src/public-api.ts b/projects/common/src/public-api.ts index 5b7a66c92..70adb5aac 100644 --- a/projects/common/src/public-api.ts +++ b/projects/common/src/public-api.ts @@ -34,6 +34,7 @@ export * from './lib/symbology/vector-symbology-editor/vector-symbology-editor.c export * from './lib/time/time-interval-input/time-interval-input.component'; export * from './lib/time/time-input/time-input.component'; export * from './lib/plots/vega-viewer/vega-viewer.component'; +export * from './lib/time/time-input/time-input.component'; // Models export * from './lib/colors/color-breakpoint.model'; diff --git a/projects/manager/src/app/app.module.ts b/projects/manager/src/app/app.module.ts index 5d1639bd8..af2ca73d8 100644 --- a/projects/manager/src/app/app.module.ts +++ b/projects/manager/src/app/app.module.ts @@ -50,6 +50,7 @@ import {ProvenanceComponent} from './provenance/provenance.component'; import {AddDatasetComponent} from './datasets/add-dataset/add-dataset.component'; import {GdalMetadataListComponent} from './datasets/loading-info/gdal-metadata-list/gdal-metadata-list.component'; import {GdalDatasetParametersComponent} from './datasets/loading-info/gdal-dataset-parameters/gdal-dataset-parameters.component'; +import {ExpireDatasetComponent} from './datasets/expire-dataset/expire-dataset.component'; export const MATERIAL_MODULES = [ MatAutocompleteModule, @@ -100,6 +101,7 @@ export const MATERIAL_MODULES = [ AddDatasetComponent, GdalMetadataListComponent, GdalDatasetParametersComponent, + ExpireDatasetComponent, ], imports: [ ...MATERIAL_MODULES, diff --git a/projects/manager/src/app/datasets/dataset-editor/dataset-editor.component.html b/projects/manager/src/app/datasets/dataset-editor/dataset-editor.component.html index 941358b43..64c537601 100644 --- a/projects/manager/src/app/datasets/dataset-editor/dataset-editor.component.html +++ b/projects/manager/src/app/datasets/dataset-editor/dataset-editor.component.html @@ -76,9 +76,9 @@ > Apply - + + + @@ -149,8 +149,6 @@ -
- -
+ } diff --git a/projects/manager/src/app/datasets/dataset-editor/dataset-editor.component.ts b/projects/manager/src/app/datasets/dataset-editor/dataset-editor.component.ts index 956cf9eac..3297e8ecf 100644 --- a/projects/manager/src/app/datasets/dataset-editor/dataset-editor.component.ts +++ b/projects/manager/src/app/datasets/dataset-editor/dataset-editor.component.ts @@ -1,10 +1,8 @@ import {ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild} from '@angular/core'; import {AbstractControl, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators} from '@angular/forms'; import {MatChipInput} from '@angular/material/chips'; -import {MatDialog} from '@angular/material/dialog'; import {MatSnackBar} from '@angular/material/snack-bar'; import { - ConfirmationComponent, DatasetsService, RasterSymbology, Symbology, @@ -25,7 +23,7 @@ import { TypedResultDescriptor, VectorResultDescriptorWithType, } from '@geoengine/openapi-client'; -import {BehaviorSubject, firstValueFrom} from 'rxjs'; +import {BehaviorSubject} from 'rxjs'; import {ProvenanceComponent} from '../../provenance/provenance.component'; import {AppConfig} from '../../app-config.service'; import {GdalMetadataListComponent} from '../loading-info/gdal-metadata-list/gdal-metadata-list.component'; @@ -70,7 +68,6 @@ export class DatasetEditorComponent implements OnChanges { private readonly datasetsService: DatasetsService, private readonly workflowsService: WorkflowsService, private readonly snackBar: MatSnackBar, - private readonly dialog: MatDialog, private readonly config: AppConfig, private readonly changeDetectorRef: ChangeDetectorRef, ) {} @@ -179,25 +176,8 @@ export class DatasetEditorComponent implements OnChanges { } } - async deleteDataset(): Promise { - const dialogRef = this.dialog.open(ConfirmationComponent, { - data: {message: 'Confirm the deletion of the dataset. This cannot be undone.'}, - }); - - const confirm = await firstValueFrom(dialogRef.afterClosed()); - - if (!confirm) { - return; - } - - try { - await this.datasetsService.deleteDataset(this.datasetListing.name); - this.snackBar.open('Dataset successfully deleted.', 'Close', {duration: this.config.DEFAULTS.SNACKBAR_DURATION}); - this.datasetDeleted.emit(); - } catch (error) { - const errorMessage = await errorToText(error, 'Deleting dataset failed.'); - this.snackBar.open(errorMessage, 'Close', {panelClass: ['error-snackbar']}); - } + async emitDeleted(): Promise { + this.datasetDeleted.emit(); } getMetaDataDefinition(): MetaDataDefinition | undefined { diff --git a/projects/manager/src/app/datasets/expire-dataset/expire-dataset.component.html b/projects/manager/src/app/datasets/expire-dataset/expire-dataset.component.html new file mode 100644 index 000000000..aaad49f2e --- /dev/null +++ b/projects/manager/src/app/datasets/expire-dataset/expire-dataset.component.html @@ -0,0 +1,42 @@ +@if (canDelete()) { + @if (isUserUpload()) { + + + Deletion + + +
+

+ Status: {{ getStatusString() }} +

+
+ +
+

Current Settings:

+
+
+ Delete Meta Data +
+
+ Expiration Time +
+
+ +
+
+
+
+ + +
+
+
+ } @else { +
+ +
+ } +} diff --git a/projects/manager/src/app/datasets/expire-dataset/expire-dataset.component.scss b/projects/manager/src/app/datasets/expire-dataset/expire-dataset.component.scss new file mode 100644 index 000000000..4775bc417 --- /dev/null +++ b/projects/manager/src/app/datasets/expire-dataset/expire-dataset.component.scss @@ -0,0 +1,26 @@ +mat-card-header { + display: flex; + justify-content: space-between; +} + +mat-card { + margin: 1rem 1rem 0rem 1rem; +} + +mat-card-content { + margin-top: 1rem; +} + +h3 { + margin: 0.5rem 0 0.5rem 0; +} + +.actions { + text-align: right; + height: 3rem; + padding: 1rem; + + button { + margin: 0.5rem; + } +} diff --git a/projects/manager/src/app/datasets/expire-dataset/expire-dataset.component.ts b/projects/manager/src/app/datasets/expire-dataset/expire-dataset.component.ts new file mode 100644 index 000000000..2722ce95f --- /dev/null +++ b/projects/manager/src/app/datasets/expire-dataset/expire-dataset.component.ts @@ -0,0 +1,173 @@ +import {AfterViewInit, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges} from '@angular/core'; +import {MatDialog} from '@angular/material/dialog'; +import moment, {Moment} from 'moment/moment'; +import {FormControl, FormGroup, UntypedFormBuilder, Validators} from '@angular/forms'; +import {DatasetsService, ConfirmationComponent, errorToText} from '@geoengine/common'; +import {DatasetAccessStatusResponse, DatasetDeletionType, ExpirationWithType, Permission} from '@geoengine/openapi-client'; +import {firstValueFrom} from 'rxjs'; +import {MatSnackBar} from '@angular/material/snack-bar'; +import {AppConfig} from '../../app-config.service'; + +export interface ExpirationForm { + deleteRecord: FormControl; + setExpire: FormControl; + expirationTime: FormControl; +} + +export enum DeletionStatus { + Available, + Expires, + Deleted, +} + +@Component({ + selector: 'geoengine-expire-dataset', + templateUrl: './expire-dataset.component.html', + styleUrl: './expire-dataset.component.scss', +}) +export class ExpireDatasetComponent implements AfterViewInit, OnChanges { + @Input() datasetName?: string; + @Output() datasetDeleted = new EventEmitter(); + + datasetStatus: DatasetAccessStatusResponse | undefined; + deletionForm!: FormGroup; + + constructor( + private formBuilder: UntypedFormBuilder, + private readonly datasetsService: DatasetsService, + private readonly snackBar: MatSnackBar, + private readonly dialog: MatDialog, + private readonly config: AppConfig, + ) {} + + ngAfterViewInit(): void { + this.setupStatus(); + } + + async ngOnChanges(changes: SimpleChanges): Promise { + if (changes.datasetName) { + this.datasetName = changes.datasetName.currentValue; + this.setupStatus(); + } + } + + async setupStatus(): Promise { + if (!this.datasetName) { + return; + } + + this.datasetStatus = await this.datasetsService.getDatasetStatus(this.datasetName); + const expiration = this.datasetStatus.expiration; + let deleteRecord = false; + if (expiration) { + deleteRecord = expiration.deletionType == DatasetDeletionType.DeleteRecordAndData; + } + let time = moment.utc(); + if (expiration && expiration.deletionTimestamp) { + time = moment(expiration.deletionTimestamp); + } + this.deletionForm = this.formBuilder.group({ + deleteRecord: [deleteRecord, Validators.required], + setExpire: [{value: expiration != null, disabled: !this.datasetStatus.isAvailable}, Validators.required], + expirationTime: [time, [Validators.required]], + }); + } + + canDelete(): boolean { + return this.datasetStatus != null && this.datasetStatus.permissions.includes(Permission.Owner); + } + + isUserUpload(): boolean { + return this.datasetStatus != null && this.datasetStatus.isUserUpload; + } + + expires(): boolean { + return this.datasetStatus != null && this.datasetStatus.isAvailable && this.datasetStatus.expiration != null; + } + + getStatusString(): string { + if (this.datasetStatus) { + if (this.datasetStatus.isAvailable && this.datasetStatus.expiration) { + return DeletionStatus[DeletionStatus.Expires]; + } else if (this.datasetStatus.expiration) { + return DeletionStatus[DeletionStatus.Deleted]; + } else { + return DeletionStatus[DeletionStatus.Available]; + } + } + return 'Unknown'; + } + + async deleteDatasetBasic(): Promise { + if (!this.datasetName) { + return; + } + + await this.datasetsService.deleteDataset(this.datasetName); + this.snackBar.open('Dataset successfully deleted.', 'Close', {duration: this.config.DEFAULTS.SNACKBAR_DURATION}); + this.datasetDeleted.emit(); + } + + async resetDeletion(): Promise { + if (this.datasetName && this.datasetStatus && this.datasetStatus.isAvailable) { + try { + await this.datasetsService.expireDataset(this.datasetName, {type: 'unsetExpire'}); + this.setupStatus(); + } catch (error) { + const errorMessage = await errorToText(error, 'Reset deletion failed.'); + this.snackBar.open(errorMessage, 'Close', {panelClass: ['error-snackbar']}); + } + } + } + + async deleteDatasetAdvanced(): Promise { + if (!this.datasetName) { + return; + } + + const dialogRef = this.dialog.open(ConfirmationComponent, { + data: {message: 'Confirm the deletion of the dataset. This cannot be undone.'}, + }); + + const confirm = await firstValueFrom(dialogRef.afterClosed()); + + if (!confirm) { + return; + } + + try { + let directDelete = true; + const formResult = this.getFormResult(); + directDelete = formResult.deletionTimestamp == undefined && formResult.deletionType == DatasetDeletionType.DeleteRecordAndData; + await this.datasetsService.expireDataset(this.datasetName, formResult); + if (directDelete) { + this.snackBar.open('Dataset successfully deleted.', 'Close', {duration: this.config.DEFAULTS.SNACKBAR_DURATION}); + this.datasetDeleted.emit(); + } else { + this.setupStatus(); + this.snackBar.open('Set dataset expiration date.', 'Close', {duration: this.config.DEFAULTS.SNACKBAR_DURATION}); + } + } catch (error) { + const errorMessage = await errorToText(error, 'Deleting dataset failed.'); + this.snackBar.open(errorMessage, 'Close', {panelClass: ['error-snackbar']}); + } + } + + private getFormResult(): ExpirationWithType { + const values = this.deletionForm.controls; + const deletionType = values.deleteRecord.value ? DatasetDeletionType.DeleteRecordAndData : DatasetDeletionType.DeleteData; + + if (!this.deletionForm.controls.setExpire.disabled && values.setExpire.value) { + return { + deletionType: deletionType, + deletionTimestamp: values.expirationTime.value.toDate(), + type: 'setExpire', + }; + } else { + return { + deletionType: deletionType, + type: 'setExpire', + }; + } + } +}