This is an automated email from the ASF dual-hosted git repository. dgnatyshyn pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/incubator-dlab.git
The following commit(s) were added to refs/heads/develop by this push: new cc42dcc [DLAB-1775]: Fixed bugs, changed file uploading (#720) cc42dcc is described below commit cc42dccfbf2989aa9e372e508318c1e502c644bc Author: Dmytro Gnatyshyn <42860905+dg1...@users.noreply.github.com> AuthorDate: Mon May 4 19:49:01 2020 +0300 [DLAB-1775]: Fixed bugs, changed file uploading (#720) --- .../resources/webapp/src/app/core/util/patterns.ts | 1 + .../bucket-browser/bucket-browser.component.html | 138 +++++++++-------- .../bucket-browser/bucket-browser.component.scss | 170 ++++++++++++++++++++- .../bucket-browser/bucket-browser.component.ts | 52 +++++-- .../bucket-browser/bucket-data.service.ts | 33 ++-- .../folder-tree/folder-tree.component.html | 25 ++- .../folder-tree/folder-tree.component.scss | 64 +++++++- .../folder-tree/folder-tree.component.ts | 48 +++++- .../detail-dialog/detail-dialog.component.html | 8 +- .../detail-dialog/detail-dialog.component.scss | 10 +- .../webapp/src/assets/styles/_dialogs.scss | 7 + 11 files changed, 453 insertions(+), 103 deletions(-) diff --git a/services/self-service/src/main/resources/webapp/src/app/core/util/patterns.ts b/services/self-service/src/main/resources/webapp/src/app/core/util/patterns.ts index ac9137f..cf8bfd3 100644 --- a/services/self-service/src/main/resources/webapp/src/app/core/util/patterns.ts +++ b/services/self-service/src/main/resources/webapp/src/app/core/util/patterns.ts @@ -24,5 +24,6 @@ export const PATTERNS = { url: '[a-zA-Z0-9.://%#&\\.@:%-_\+~#=]*\.[^\s]*[a-zA-Z0-9]/+', nodeCountPattern: '^[1-9]\\d*$', integerRegex: '^[0-9]*$', + folderRegex: `[A-Za-z0-9]+$`, fullUrl: /^(http?|ftp|https):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+([.:])(\d{4}|com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*\/$/ }; diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.html index 1de656e..1514359 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.html +++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.html @@ -23,11 +23,12 @@ <button type="button" class="close" (click)="dialogRef.close()">×</button> </header> +<!-- <div class="dialog-content tabs">--> <div class="dialog-content tabs" [hidden]="!path" > <div class="submit m-bott-10 m-top-10"> <span [matTooltip]="'You have not permission to upload data'" matTooltipPosition="above" matTooltipDisabled="{{this.bucketStatus.upload}}"> - <button mat-raised-button type="button" class="butt action" [disabled]="!this.bucketStatus.upload"> - <input [ngClass]="{'not-allowed': !this.bucketStatus.upload}" type="file" (change)="handleFileInput($event)"> + <button mat-raised-button type="button" class="butt action" [disabled]="!this.bucketStatus.upload || allDisable"> + <input [ngClass]="{'not-allowed': !this.bucketStatus.upload}" type="file" (change)="handleFileInput($event)" title=""> Add file </button> </span> @@ -36,96 +37,113 @@ mat-raised-button type="button" class="butt action" - (click)="folderTreeComponent.addNewItem(selectedFolder, '', false)" - [disabled]="!this.bucketStatus.upload" + (click)="createFolder(selectedFolder)" + [disabled]="!this.bucketStatus.upload || allDisable" > Create folder </button> </span> + <span [matTooltip]="'You have not permission to delete data'" matTooltipPosition="above" matTooltipDisabled="{{this.bucketStatus.delete}}"> + <button + type="button" + class="butt" + mat-raised-button + (click)="fileAction('delete')" + [disabled]="!selected?.length || !this.bucketStatus.delete || allDisable" + + > + Delete + </button> + </span> + + <span [matTooltip]="'You have not permission to download data'" matTooltipPosition="above" matTooltipDisabled="{{this.bucketStatus.download}}"> + <button + type="button" class="butt" + mat-raised-button + (click)="fileAction('download')" + [disabled]="!selected?.length || !this.bucketStatus.download || allDisable" + > + Download + </button> + </span> + + +<!-- <button type="button" class="butt" mat-raised-button [disabled]="this.addedFiles.length === 0" (click)="uploadNewFile()">Upload</button>--> </div> - <p class="path"><span>Bucket path:</span><span class="url"> {{path}}</span></p> + <p class="path"><span>Bucket path:</span><span class="url ellipsis"> {{path}}</span></p> <div class="backet-wrapper" id="scrolling"> <div class="navigation"> - <dlab-folder-tree (showFolderContent)=onFolderClick($event)> </dlab-folder-tree> + <dlab-folder-tree + (showFolderContent)=onFolderClick($event) + (disableAll) = dissableAll($event) + [folders] = folders + > </dlab-folder-tree> </div> <div class="directory"> - <ul class="folder-tree" *ngIf="!addedFiles.length"> + <div class="folder-item t_header" *ngIf="folderItems.length && !addedFiles.length"> + <div class="folder-item-wrapper header-wrapper folder-tree"> + <div class="name"><span class="th_name">Name</span></div> + <div class="size"><span class="th_size">Size</span></div> + <div class="date"><span class="th_date">Last modified</span></div> + </div> + </div> +<!-- <ul class="folder-tree" *ngIf="!addedFiles.length">--> + <ul class="folder-tree"> <li *ngFor="let file of folderItems" class="folder-item" > - - <div class="folder-item-wrapper" *ngIf="file.children" (click)="showItem(file)"> - <div class="name name-folder"><i class="material-icons folder-icon" >folder</i><span>{{file.item}}</span></div> - <div class="size"></div> - <div class="progress-wrapper"> - <div class="progres" *ngIf="file.isSelected"><div class="bar"></div></div> - </div> + <div class="folder-item-wrapper" *ngIf="file.children && file.item" (click)="showItem(file)"> + <div class="name name-folder"><i class="material-icons folder-icon" >folder</i><span class="name-wrap">{{file.item}}</span></div> + <div class="size size-folder">-</div> + <div class="date" *ngIf="!file.isDownloading">-</div> +<!-- <div class="progress-wrapper">--> +<!-- <div class="progres" *ngIf="file.isSelected"><div class="bar"></div></div>--> +<!-- </div>--> </div> - <div class="folder-item-wrapper" (click)="toggleSelectedFile(file)" *ngIf="!file.children"> + <div class="folder-item-wrapper" (click)="toggleSelectedFile(file)" *ngIf="!file.children" [ngClass]="{'no-select': !this.bucketStatus.download && !this.bucketStatus.delete}"> <div class="name name-file"> - <span *ngIf="this.bucketStatus.download || this.bucketStatus.download" class="empty-checkbox" [ngClass]="{'checked': file.isSelected}" (click)="toggleSelectedFile(file);$event.stopPropagation()" > + <span *ngIf="this.bucketStatus.download || this.bucketStatus.delete" class="empty-checkbox" [ngClass]="{'checked': file.isSelected}" (click)="toggleSelectedFile(file);$event.stopPropagation()" > <span class="checked-checkbox" *ngIf="file.isSelected"></span> </span> - <span class="item-name" [ngClass]="{'removed-checkbox': !this.bucketStatus.download && !this.bucketStatus.download}">{{file.item}}</span> + <span class="item-name name-wrap" [ngClass]="{'removed-checkbox': !this.bucketStatus.download && !this.bucketStatus.delete}">{{file.item}}</span> </div> - <div class="size">{{file.size.size}}</div> - <div class="size" *ngIf="!file.isDownloading">{{file.size.lastModifiedDate }}</div> + <div class="size">{{file.size?.size}}</div> + <div class="date" *ngIf="!file.isDownloading">{{file.size?.lastModifiedDate }}</div> <div class="progress-wrapper" *ngIf="file.isDownloading"> <mat-progress-bar mode="indeterminate"></mat-progress-bar> </div> </div> - - </li> - </ul> - <ul class="folder-tree"> - <li *ngFor="let file of addedFiles" class="folder-item"> - <div class="folder-item-wrapper"> - <div class="name">{{file.item}}</div> - <div class="size">{{file.size}}MB</div> - <div class="progress-wrapper"> - <mat-progress-bar mode="indeterminate" *ngIf="isUploading"></mat-progress-bar> - </div> - <div (click)="deleteAddedFile(file)"><i class="material-icons close">close</i></div> - </div> </li> </ul> </div> </div> <div class="text-center m-top-30 m-bott-30"> <button type="button" class="butt" mat-raised-button (click)="this.dialogRef.close()">Close</button> - <span [matTooltip]="'You have not permission to download data'" matTooltipPosition="above" matTooltipDisabled="{{this.bucketStatus.download}}"> - <button - *ngIf="!this.addedFiles.length" - type="button" class="butt butt-success" - mat-raised-button - (click)="fileAction('download')" - [disabled]="!selected?.length || !this.bucketStatus.download" - > - Download - </button> - </span> - <span [matTooltip]="'You have not permission to delete data'" matTooltipPosition="above" matTooltipDisabled="{{this.bucketStatus.delete}}"> - <button - *ngIf="!this.addedFiles.length" - type="button" - class="butt butt-success" - mat-raised-button - (click)="fileAction('delete')" - [disabled]="!selected?.length || !this.bucketStatus.delete" - - > - Delete - </button> - </span> - - <button *ngIf="this.addedFiles.length !== 0" type="button" class="butt butt-success" mat-raised-button (click)="uploadNewFile()">Upload</button> </div> </div> - + <div class="upload-window" *ngIf="isUploadWindowOpened"> + <header class="upload-header"> + <h4 class="modal-title">Upload files</h4> + <button type="button" class="close" (click)="closeUploadWindow()">×</button> + </header> + <ul class="upload-files"> + <li *ngFor="let file of addedFiles" class="file"> + <div class="name ellipsis" matTooltip="{{file.name}}">{{file.name}}</div> + <div class="upload-path ellipsis" matTooltip="{{file.path}}">{{file.path}}</div> + <div class="size">{{file.size}}MB</div> + <div class="state"> + <button mat-raised-button (click)="uploadNewFile(file)" *ngIf="!file.isUploading && !file.doneUploading && !file.errorUploading">Upload</button> + <mat-progress-bar mode="indeterminate" *ngIf="file.isUploading"></mat-progress-bar> + <span *ngIf="file.uploaded">Uploaded</span> + <span *ngIf="file.errorUploading" class="error">Uploading error</span> + </div> + <div class="remove"><i (click)="deleteAddedFile(file)" class="material-icons close">close</i></div> + </li> + </ul> + </div> <div class="loading-block" *ngIf="!path"> <div class="uploading"> <p>Please wait until DLab loads bucket: <span class="strong">{{data.bucket}}</span>...</p> <mat-progress-bar mode="indeterminate"></mat-progress-bar> </div> </div> - </div> diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.scss index eb0c55e..87a2461 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.scss +++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.scss @@ -18,6 +18,8 @@ */ .bucket-browser { + font-size: 14px; + font-weight: 400; .loading-block{ width: 80%; margin: 20% auto 0 auto; @@ -35,11 +37,17 @@ } .path{ - margin: 0 4px 10px 4px; + margin: 0 4px 20px 4px; padding: 4px 4px 4px 20px; color: rgba(0,0,0,.87); + overflow: hidden; + white-space: nowrap; + display: flex; .url{ font-weight: 600; + overflow: hidden; + display: block; + padding-left: 10px; } } bottom: 0; @@ -89,24 +97,39 @@ border-right: 2px solid rgba(0,0,0,.12); max-height: 500px; overflow: auto; + padding-top: 6px; .folder-tree{ .folder{ line-height: 30px; } } + .mat-tree-node{ + min-height: auto; + } } .directory{ max-height: 500px; - overflow: auto; flex: 2; + font-size: 14px; + font-weight: 400; + position: relative; .folder-tree{ + overflow: auto; + max-height: 100%; .name{ flex:2; display: flex; align-items: center; + overflow: hidden; + .name-wrap { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-right: 10px; + } &-folder{ span{ padding-left: 10px; @@ -124,6 +147,15 @@ } .size{ flex:1; + &-folder{ + padding-left: 34px; + } + } + .date{ + flex:1; + .th_date{ + font-size: 13px; + } } .progress-wrapper{ flex:1; @@ -175,11 +207,32 @@ .folder-tree { padding: 20px 30px; + padding-top: 6px; } .folder-item{ display: flex; align-items: center; + &.t_header{ + top: -27px; + position: absolute; + left: 0; + right: 0; + + .th_name{ + padding-left: 35px; + font-size: 11px; + } + + .th_size{ + font-size: 11px; + padding-left: 3px; + } + + .th_date{ + font-size: 11px !important; + } + } .folder-item-wrapper{ width: 100%; display: flex; @@ -190,7 +243,7 @@ i{ color: rgb(232, 232, 232); } - &:hover{ + &:hover:not(.header-wrapper){ color: #00bcd4; transition: .3s ease-in-out; i{ @@ -206,11 +259,22 @@ } } } + &.no-select{ + pointer-events: none; + .name-wrap{ + padding-left: 29px !important; + } + } } } } +input[type='file'] { + opacity:0 +} + .empty-checkbox { + min-width: 16px; width: 16px; height: 16px; border-radius: 2px; @@ -241,9 +305,109 @@ .check-box { background-color: red; } +} + +.upload-window{ + box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.42), 0 24px 20px 3px rgba(0, 0, 0, 0.42), 0 9px 20px 8px rgba(0, 0, 0, 0.42); + position: fixed; + right: 0; + bottom: 0; + background-color: white; + width: 700px; + min-height: 100px; + color: black; + .upload-header{ + padding-left: 30px; + background: #f6fafe; + height: 30px; + line-height: 30px; + position: relative; + .modal-title { + width: 90%; + margin: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-weight: 600; + color: #455c74; + font-size: 14px; + background: #f6fafe; + } + .close{ + position: absolute; + top: 0; + right: 0; + height: 30px; + width: 30px; + font-size: 14px; + font-weight: 300; + border: 0; + background: none; + color: #577289; + outline: none; + cursor: pointer; + transition: all 0.45s ease-in-out; + &:hover{ + color: #36afd5; + } + } + } + .upload-files{ + padding: 5px; + .file{ + padding: 2px; + display: flex; + font-size: 12px; + .name{ + flex:12; + padding-right: 5px; + } + + .upload-path{ + flex:12; + padding-right: 5px; + } + + .size{ + flex:4; + } + .state{ + flex:6; + display: flex; + justify-content: center; + align-items: center; + .mat-raised-button{ + width: 60px; + padding: 5px; + border-radius: 0; + font-style: normal; + font-weight: 600; + font-size: 11px; + font-family: "Open Sans", sans-serif; + color: #577289; + line-height: 8px; + } + } + .remove{ + flex:1; + display: flex; + align-items: center; + .close{ + color: #577289; + font-size: 14px; + cursor: pointer; + &:hover{ + color: #f1696e; + } + } + } + } + } } + + @media only screen and (max-height: 840px) { .backet-wrapper { height: 45vh; diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.ts index 706a8f1..88e185b 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.ts +++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.ts @@ -40,13 +40,17 @@ export class BucketBrowserComponent implements OnInit { public pathInsideBucket = ''; public bucketName = ''; public endpoint = ''; + public isUploadWindowOpened = false; @ViewChild(FolderTreeComponent, {static: true}) folderTreeComponent; public selectedFolder: any; private isUploading: boolean; - private selected: any[]; + public selected: any[]; private uploadForm: FormGroup; public bucketStatus; + public allDisable: boolean; + public folders: any[]; + constructor( @Inject(MAT_DIALOG_DATA) public data: any, @@ -81,9 +85,15 @@ export class BucketBrowserComponent implements OnInit { const file = event.target.files[0]; this.uploadForm.get('file').setValue(file); const newAddedFiles = Object['values'](event.target.files).map(v => ( - {item: v['name'], 'size': (v['size'] / 1048576).toFixed(2)} as unknown as TodoItemNode )); - this.addedFiles = [...newAddedFiles]; + {name: v['name'], file: file, 'size': (v['size'] / 1048576).toFixed(2), path: this.path, isUploading: false, uploaded: false, errorUploading: false})); + this.addedFiles = [...this.addedFiles, ...newAddedFiles]; + this.isUploadWindowOpened = !!this.addedFiles.length; } + event.target.files = []; + } + + public closeUploadWindow() { + this.isUploadWindowOpened = false; } @@ -98,19 +108,24 @@ export class BucketBrowserComponent implements OnInit { } filesPicked(files) { - Array.prototype.forEach.call(files, file => { this.addedFiles.push(file.webkitRelativePath); }); } + public dissableAll(event) { + this.allDisable = event; + } + public onFolderClick(event) { + this.folderItems.forEach(item => item.isSelected = false); + this.selected = this.folderItems.filter(item => item.isSelected); this.selectedFolder = event.flatNode; this.folderItems = event.element ? event.element.children : event.children; if (this.folderItems) { - const folders = this.folderItems.filter(v => v.children).sort((a, b) => a.item > b.item ? 1 : -1); + this.folders = this.folderItems.filter(v => v.children).sort((a, b) => a.item > b.item ? 1 : -1); const files = this.folderItems.filter(v => !v.children).sort((a, b) => a.item > b.item ? 1 : -1); - this.folderItems = [...folders, ...files]; + this.folderItems = [...this.folders, ...files]; this.path = event.path; this.pathInsideBucket = this.path.indexOf('/') !== -1 ? this.path.slice(this.path.indexOf('/') + 1) + '/' : ''; this.bucketName = this.path.substring(0, this.path.indexOf('/')) || this.path; @@ -123,25 +138,34 @@ export class BucketBrowserComponent implements OnInit { this.addedFiles.splice(this.addedFiles.indexOf(file), 1); } - private uploadNewFile() { - const path = `${this.pathInsideBucket}${this.uploadForm.get('file').value.name}`; - + private uploadNewFile(file) { + const path = `${file.path}/${file.name}`; const formData = new FormData(); - formData.append('file', this.uploadForm.get('file').value); + formData.append('file', file.file); formData.append('object', path); formData.append('bucket', this.bucketName); formData.append('endpoint', this.endpoint); - this.isUploading = true; + file.isUploading = true; + console.log(formData); this.bucketBrowserService.uploadFile(formData) .subscribe(() => { this.bucketDataService.refreshBucketdata(this.data.bucket, this.data.endpoint); - this.addedFiles = []; - this.isUploading = false; + file.isUploading = false; + file.uploaded = true; this.toastr.success('File successfully uploaded!', 'Success!'); - }, error => this.toastr.error(error.message || 'File uploading error!', 'Oops!') + }, error => { + this.toastr.error(error.message || 'File uploading error!', 'Oops!'); + file.errorUploading = true; + file.isUploading = false; + } ); } + public createFolder(folder) { + this.allDisable = true; + this.folderTreeComponent.addNewItem(folder, '', false); + } + public fileAction(action) { this.selected = this.folderItems.filter(item => item.isSelected); const selected = this.folderItems.filter(item => item.isSelected)[0]; diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-data.service.ts b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-data.service.ts index f87c575..7c92a8a 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-data.service.ts +++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-data.service.ts @@ -21,8 +21,8 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject} from 'rxjs'; import {BucketBrowserService, TodoItemNode} from '../../core/services/bucket-browser.service'; -const array = [{'bucket': 'ofuks-1304-prj1-local-bucket', 'object': '4.txt', 'size': '18 bytes', 'creationDate': '21-4-2020 11:36:36'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': '5.txt', 'size': '18 bytes', 'creationDate': '21-4-2020 11:56:46'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': 'Untitled', 'size': '5 bytes', 'creationDate': '13-4-2020 03:39:11'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': 'adasdas', 'size': '1 KB', 'creationDate': '15-4-2020 02:17 [...] - +const array = [{'bucket': 'ofuks-1304-prj1-local-bucket', 'object': '4.txt/dsafaraorueajkegrgavhsfnvgahsfgsdjfhagsdjfg497frgfhsdajfsgdafjs', 'size': '18 bytes', 'lastModifiedDate': '21-4-2020 11:36:36'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': '51.txt', 'size': '18 bytes', 'lastModifiedDate': '21-4-2020 11:56:46'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': 'Untitlsed', 'size': '5 bytes', 'lastModifiedDate': '13-4-2020 03:39:11'}, {'bucket': 'ofuks-1304-prj1-local- [...] +// const array = []; @Injectable() export class BucketDataService { @@ -51,9 +51,16 @@ export class BucketDataService { } public buildFileTree(obj: {[key: string]: any}, level: number): TodoItemNode[] { return Object.keys(obj).reduce<TodoItemNode[]>((accumulator, key) => { + if (key === '') { + return accumulator; + } const value = obj[key]; const node = new TodoItemNode(); node.item = key; + if (value === '') { + node.children = this.buildFileTree({}, level + 1); + return accumulator.concat(node); + } if (Object.keys(value).length) { if (typeof value === 'object') { node.children = this.buildFileTree(value, level + 1); @@ -72,7 +79,7 @@ export class BucketDataService { if (isFile) { parent.children.push(name as TodoItemNode); } else { - parent.children.unshift({item: name, children: []} as TodoItemNode); + parent.children.unshift({item: name ? name + '/' : name, children: []} as TodoItemNode); this._bucketData.next(this.data); } } @@ -83,6 +90,11 @@ export class BucketDataService { this._bucketData.next(this.data); } + public removeItem(parent) { + parent.children.shift(); + this._bucketData.next(this.data); + } + public processFiles = (files, target) => { let pointer = target; files.forEach((file) => { @@ -94,15 +106,18 @@ export class BucketDataService { } public processFolderArray = (acc, curr) => { - const files = curr.object.split('/').filter(x => x.length > 0); + // const files = curr.object.split('/').filter(x => x.length > 0); + const files = curr.object.split('/'); this.processFiles(files, acc); return acc; } - public convertToFolderTree = (data) => data - .reduce( - this.processFolderArray, - {} - ) + public convertToFolderTree = (data) => { + const finalData = data.reduce(this.processFolderArray, {}); + if (Object.keys(finalData).length === 0) { + return ''; + } + return finalData; + } } diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.html index b663e78..4e29cc8 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.html +++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.html @@ -7,13 +7,24 @@ {{node.item}} </mat-tree-node> - <mat-tree-node *matTreeNodeDef="let node; when: hasNoContent" matTreeNodePadding> - <button mat-icon-button disabled></button> - <mat-form-field> - <mat-label>New folder</mat-label> - <input matInput #itemValue> - </mat-form-field> - <button mat-button (click)="saveNode(node, itemValue.value)">Save</button> + <mat-tree-node *matTreeNodeDef="let node; when: hasNoContent" matTreeNodePadding class="input-node"> + <form class="add-folder-form"> + <mat-form-field> + <mat-label>New folder</mat-label> + <input matInput #itemValue [formControl]="folderFormControl" [errorStateMatcher]="matcher"> + <mat-error *ngIf="!folderFormControl.hasError('required') && !folderFormControl.hasError('isDuplicate')"> + Folder name can only contain latin letters, numbers end special characters exepting #, ?, /, \, " + </mat-error> + <mat-error *ngIf="folderFormControl.hasError('required')">isDuplicate + Folder name is <strong>required</strong> + </mat-error> + <mat-error *ngIf="folderFormControl.hasError('isDuplicate')"> + Folder with this name already exist + </mat-error> + </mat-form-field> + <button (click)="saveNode(node, itemValue.value)" [ngClass]="{'check': folderFormControl.valid && folderFormControl.dirty}" mat-icon-button class="btn action-btn" [disabled]="!folderFormControl.valid || !folderFormControl.dirty"><span><i class="material-icons ">check</i></span></button> + <button (click)="removeItem(node)" mat-icon-button class="btn close action-btn"><span ><i class="material-icons ">close</i></span></button> + </form> </mat-tree-node> <mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding> diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.scss index e9902af..80d51f9 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.scss +++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.scss @@ -1,6 +1,14 @@ .folder{ padding-left: 5px; + max-width: 150px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.mat-tree{ + font-family: 'Open Sans', sans-serif; } .folder-icon{ @@ -24,10 +32,23 @@ } -.mat-tree-node{ +.add-folder-form{ + display: flex; + align-items: center; +} + +.mat-tree-node:not(.input-node){ cursor: pointer; transition: .3s; overflow: unset; + min-height: auto; + button.mat-icon-button{ + width: 25px; + height: 25px; + line-height: 28px; + //display: flex; + //align-items: center; + } &:hover{ color: #00bcd4; i{ @@ -36,3 +57,44 @@ } } +.input-node { + overflow: unset; + + button.mat-icon-button { + &.action-btn { + color: lightgrey; + font-size: 20px; + cursor: not-allowed; + } + + &.check { + color: #49af38; + cursor: pointer; + + &:hover { + border-color: #49af38; + background: #f9fafb; + } + } + + &.close { + color: #f1696e; + cursor: pointer; + + &:hover { + border-color: #f1696e; + background: #f9fafb; + } + } + } + + .mat-error { + background-color: #ffffff; + } +} + + + + + + diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.ts index 179a4c1..5fdfda9 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.ts +++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.ts @@ -1,12 +1,20 @@ -import {Component, OnInit, AfterViewInit, Output, EventEmitter, OnDestroy} from '@angular/core'; +import {Component, OnInit, AfterViewInit, Output, EventEmitter, OnDestroy, Input} from '@angular/core'; import {SelectionModel} from '@angular/cdk/collections'; import {FlatTreeControl} from '@angular/cdk/tree'; import {MatTreeFlatDataSource, MatTreeFlattener} from '@angular/material/tree'; import {BucketBrowserService, TodoItemFlatNode, TodoItemNode} from '../../../core/services/bucket-browser.service'; import {BucketDataService} from '../bucket-data.service'; import {Subscription} from 'rxjs'; - - +import {FormControl, FormGroupDirective, NgForm, Validators} from '@angular/forms'; +import {ErrorStateMatcher} from '@angular/material/core'; +import {PATTERNS} from '../../../core/util'; + +export class MyErrorStateMatcher implements ErrorStateMatcher { + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const isSubmitted = form && form.submitted; + return !!(control && control.invalid && (control.dirty)); + } +} @Component({ selector: 'dlab-folder-tree', @@ -17,6 +25,9 @@ import {Subscription} from 'rxjs'; export class FolderTreeComponent implements OnInit, OnDestroy { @Output() showFolderContent: EventEmitter<any> = new EventEmitter(); + @Output() disableAll: EventEmitter<any> = new EventEmitter(); + @Input() folders; + private folderTreeSubs; private path = []; private selectedFolder: TodoItemFlatNode; @@ -29,6 +40,7 @@ export class FolderTreeComponent implements OnInit, OnDestroy { private treeFlattener: MatTreeFlattener<TodoItemNode, TodoItemFlatNode>; public dataSource: MatTreeFlatDataSource<TodoItemNode, TodoItemFlatNode>; + private checklistSelection = new SelectionModel<TodoItemFlatNode>(true /* multiple */); constructor( @@ -85,6 +97,21 @@ export class FolderTreeComponent implements OnInit, OnDestroy { this.bucketDataService._bucketData.next([]); } + folderFormControl = new FormControl('', [ + Validators.required, + Validators.pattern(PATTERNS.folderRegex), + this.duplicate.bind(this) + ]); + + matcher = new MyErrorStateMatcher(); + + private duplicate(control) { + if (control && control.value) { + const isDublicat = this.folders.slice(1).some(folder => folder.item.toLocaleLowerCase() === control.value.toLowerCase()); + return isDublicat ? { isDuplicate: true } : null; + } + } + private showItem(el) { if (el) { this.treeControl.expand(el); @@ -203,9 +230,24 @@ export class FolderTreeComponent implements OnInit, OnDestroy { this.treeControl.expand(node); } + private removeItem(node: TodoItemFlatNode) { + const parentNode = this.flatNodeMap.get(this.getParentNode(node)); + this.bucketDataService.removeItem(parentNode!); + this.resetForm(); + } + private saveNode(node: TodoItemFlatNode, itemValue: string) { const nestedNode = this.flatNodeMap.get(node); this.bucketDataService.updateItem(nestedNode!, itemValue); + this.resetForm(); + this.folderFormControl.updateValueAndValidity(); + this.folderFormControl.markAsPristine(); + } + private resetForm(){ + this.folderFormControl.setValue(''); + this.folderFormControl.updateValueAndValidity(); + this.folderFormControl.markAsPristine(); + this.disableAll.emit(false); } } diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.html index cd39c46..09351f0 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.html +++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.html @@ -68,9 +68,9 @@ [ngClass]="{'not-allow': !this.bucketStatus.view}" > <p *ngIf="DICTIONARY[PROVIDER].cloud_provider === 'azure' && notebook.account_name">{{ DICTIONARY[PROVIDER].account }} - <span class="bucket-info" [ngClass]="{'not-allow': !this.bucketStatus.view}">{{ notebook.account_name}}</span></p> + <span class="bucket-info bucket-link" [ngClass]="{'not-allow': !this.bucketStatus.view}">{{ notebook.account_name}}</span></p> <p *ngIf="notebook.bucket_name">{{ DICTIONARY[PROVIDER].container }} <span - class="bucket-info" [ngClass]="{'not-allow': !this.bucketStatus.view}">{{ notebook.bucket_name }}</span></p> + class="bucket-info bucket-link" [ngClass]="{'not-allow': !this.bucketStatus.view}">{{ notebook.bucket_name }}</span></p> </div> <p>Shared endpoint bucket: </p> <div class="links_block" (click)="bucketBrowser(notebook.shared_bucket_name, notebook.endpoint, this.bucketStatus.view)" @@ -80,9 +80,9 @@ [ngClass]="{'not-allow': !this.bucketStatus.view}" > <p *ngIf="DICTIONARY[PROVIDER].cloud_provider === 'azure' && notebook.shared_account_name">{{ DICTIONARY[PROVIDER].account }} - <span class="bucket-info" [ngClass]="{'not-allow': !this.bucketStatus.view}">{{ notebook.shared_account_name}}</span></p> + <span class="bucket-info bucket-link" [ngClass]="{'not-allow': !this.bucketStatus.view}">{{ notebook.shared_account_name}}</span></p> <p *ngIf="notebook.shared_bucket_name">{{ DICTIONARY[PROVIDER].container }} - <span class="bucket-info" [ngClass]="{'not-allow': !this.bucketStatus.view}">{{ notebook.shared_bucket_name }}</span></p> + <span class="bucket-info bucket-link" [ngClass]="{'not-allow': !this.bucketStatus.view}">{{ notebook.shared_bucket_name }}</span></p> </div> <br /> diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.scss index 24d273f..2f36e20 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.scss +++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.scss @@ -89,6 +89,12 @@ cursor: pointer; } -.not-allow{ - cursor: not-allowed; +.bucket-link{ + color: #35afd5; + &.not-allow{ + cursor: not-allowed; + color: $blue-grey-color; + } } + + diff --git a/services/self-service/src/main/resources/webapp/src/assets/styles/_dialogs.scss b/services/self-service/src/main/resources/webapp/src/assets/styles/_dialogs.scss index c5ac335..4fa2ae0 100644 --- a/services/self-service/src/main/resources/webapp/src/assets/styles/_dialogs.scss +++ b/services/self-service/src/main/resources/webapp/src/assets/styles/_dialogs.scss @@ -343,6 +343,13 @@ mat-dialog-container { overflow: auto; } +.bucket-browser{ + .mat-form-field-appearance-legacy .mat-form-field-subscript-wrapper { + width: calc(100% + 77px); + } +} + + @media screen and (max-width: 1280px) { .modal-fullscreen { max-width: 100vw !important; --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@dlab.apache.org For additional commands, e-mail: commits-h...@dlab.apache.org