Skip to content
This repository has been archived by the owner on Nov 22, 2019. It is now read-only.

Commit

Permalink
Provide a Download action for a dataservice
Browse files Browse the repository at this point in the history
* api.service.ts
 * Provides an isJSON checker function
 * Provides a msg extraction function from a response
 * Log the error in the handleError function

* dataservice[s]-*
 * Plumbs in the Download action

* dataservices.service.ts
 * Fetches the dataservice as a base64 string then converts it to its jar
   file then uses FileSaver to save the file to the local filesystem.
  • Loading branch information
phantomjinx committed Mar 26, 2018
1 parent 983ba46 commit 87ef7ae
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 4 deletions.
1 change: 1 addition & 0 deletions ngapp/package.json
Expand Up @@ -23,6 +23,7 @@
"@angular/router": "^4.4.6",
"core-js": "^2.5.3",
"express": "^4.16.2",
"file-saver": "^1.3.3",
"ng2-codemirror": "^1.1.3",
"ngx-bootstrap": "^2.0.2",
"patternfly": "^3.41.0",
Expand Down
40 changes: 39 additions & 1 deletion ngapp/src/app/core/api.service.ts
Expand Up @@ -59,8 +59,46 @@ export abstract class ApiService {
return this.appSettings.getKomodoUserWorkspacePath();
}

protected isJSON(item: string): boolean {
item = typeof item !== "string" ? JSON.stringify(item) : item;

try {
item = JSON.parse(item);
} catch (e) {
return false;
}

if (typeof item === "object" && item !== null) {
return true;
}

return false;
}

protected msgFromResponse(response: Response): string {
let msg = "";

let body = response.text();
if (! this.isJSON(response.text())) {
msg = response.text();
} else {
let body = response.json();

if (body.message)
msg = body.message;
else if (body.error)
msg = body.error;
}

if (msg.length === 0 ) {
return 'unknown error';
}

return msg;
}

protected handleError(error: Response): ErrorObservable {
this.logger.error( this.constructor.name + "::handleError" );
this.logger.error( this.constructor.name + "::handleError => " + this.msgFromResponse(error));
return Observable.throw(error);
}

Expand Down
Expand Up @@ -16,6 +16,12 @@
font-family: "FontAwesome";
}

.card-toolbar .secondary-action[title="Download"]:before {
color: var(--card-action-icon-color);
content: "\f019";
font-family: "FontAwesome";
}

.object-card .list-pf,
.object-card-selected .list-pf {
border: none;
Expand Down
Expand Up @@ -38,10 +38,12 @@ export class DataserviceCardComponent implements DoCheck, OnInit {
public static readonly quickLookDataserviceEvent = "quickLook";
public static readonly refreshDataserviceEvent = "activate";
public static readonly testDataserviceEvent = "test";
public static readonly downloadDataserviceEvent = "download";

public readonly editEvent = DataserviceCardComponent.editDataserviceEvent;
public readonly quickLookEvent = DataserviceCardComponent.quickLookDataserviceEvent;
public readonly testEvent = DataserviceCardComponent.testDataserviceEvent;
public readonly downloadEvent = DataserviceCardComponent.downloadDataserviceEvent;

@Input() public dataservice: Dataservice;
@Input() public selectedDataservices: Dataservice[];
Expand All @@ -61,6 +63,8 @@ export class DataserviceCardComponent implements DoCheck, OnInit {
private readonly publishActionIndex = 2; // index in moreActions
private readonly refreshActionId = "refresh";
private readonly refreshActionIndex = 1; // index in moreActions
private readonly downloadActionId = "download";
private readonly downloadActionIndex = 4; // index in moreActions

private isLoading = true;
private logger: LoggerService;
Expand Down Expand Up @@ -123,6 +127,8 @@ export class DataserviceCardComponent implements DoCheck, OnInit {
this.onClick( DataserviceCardComponent.publishDataserviceEvent );
} else if ( action.id === this.refreshActionId ) {
this.onClick( DataserviceCardComponent.refreshDataserviceEvent );
} else if ( action.id === this.downloadActionId ) {
this.onClick( DataserviceCardComponent.downloadDataserviceEvent );
} else {
this.logger.error( "Action '" + action.id + "' not handled." );
}
Expand All @@ -143,6 +149,7 @@ export class DataserviceCardComponent implements DoCheck, OnInit {
this.actionConfig.moreActions[ this.deleteActionIndex ].disabled = this.isLoading;
this.actionConfig.moreActions[ this.publishActionIndex ].disabled = this.isLoading;
this.actionConfig.moreActions[ this.refreshActionIndex ].disabled = this.isLoading;
this.actionConfig.moreActions[ this.downloadActionIndex ].disabled = this.isLoading;
}

this.cardConfig.action.iconStyleClass = this.detailsIconStyle;
Expand Down Expand Up @@ -171,6 +178,12 @@ export class DataserviceCardComponent implements DoCheck, OnInit {
title: "Publish",
tooltip: "Publish"
},
{
disabled: true,
id: this.downloadActionId,
title: "Download",
tooltip: "Download"
},
{
disabled: true,
id: this.deleteActionId,
Expand Down
Expand Up @@ -39,6 +39,7 @@ export class DataservicesCardsComponent {
@Output() public deleteDataservice: EventEmitter<string> = new EventEmitter<string>();
@Output() public editDataservice: EventEmitter<string> = new EventEmitter<string>();
@Output() public quickLookDataservice: EventEmitter<string> = new EventEmitter<string>();
@Output() public downloadDataservice: EventEmitter<string> = new EventEmitter<string>();

public logger: LoggerService;

Expand Down Expand Up @@ -73,6 +74,10 @@ export class DataservicesCardsComponent {
case DataserviceCardComponent.testDataserviceEvent:
this.testDataservice.emit( event.dataserviceName );
break;
case DataserviceCardComponent.downloadDataserviceEvent:
this.downloadDataservice.emit ( event.dataserviceName );
this.logger.error( "Download dataservice event type of '" + event.eventType + "'" );
break;
default:
this.logger.error( "Unhandled event type of '" + event.eventType + "'" );
break;
Expand Down
4 changes: 2 additions & 2 deletions ngapp/src/app/dataservices/dataservices.component.html
Expand Up @@ -89,7 +89,8 @@ <h1 class="card-pf-title">
(activateDataservice)="onActivate($event)" (testDataservice)="onTest($event)"
(publishDataservice)="onPublish($event)" (deleteDataservice)="onDelete($event)"
(editDataservice)="onEdit($event)" (quickLookDataservice)="onQuickLook($event)"
(dataserviceSelected)="onSelected($event)" (dataserviceDeselected)="onDeselected($event)"></app-dataservices-cards>
(downloadDataservice)="onDownload($event)" (dataserviceSelected)="onSelected($event)"
(dataserviceDeselected)="onDeselected($event)"></app-dataservices-cards>
</div>
</div>
<div class="col-md-12 {{resultsAreaCss}}" *ngIf="showResults">
Expand All @@ -112,4 +113,3 @@ <h1 class="card-pf-title">
<app-confirm-delete (deleteSelected)="onDeleteDataservice()">
<p i18n="@@dataservices.doYouReallyWantToDeleteSelectedDataservice" >Do you really want to delete the selected Dataservice?</p>
</app-confirm-delete>

36 changes: 35 additions & 1 deletion ngapp/src/app/dataservices/dataservices.component.ts
Expand Up @@ -60,9 +60,12 @@ export class DataservicesComponent extends AbstractPageComponent implements OnIn
public readonly exportInProgressHeader: string = "Publishing: ";
public readonly exportSuccessHeader: string = "Publish Succeeded: ";
public readonly exportFailedHeader: string = "Publish Failed: ";

public readonly connectionsLoadedTag = "connections";

public readonly downloadInProgressHeader: string = "Downloading: ";
public readonly downloadSuccessHeader: string = "Download Succeeded: ";
public readonly downloadFailedHeader: string = "Download Failed: ";

public filterConfig: FilterConfig;
public filtersText = "";
public items: Dataservice[];
Expand Down Expand Up @@ -403,6 +406,37 @@ export class DataservicesComponent extends AbstractPageComponent implements OnIn
});
}

/**
* Handle Download of the specified Dataservice
* @param {string} svcName
*/
public onDownload(svcName: string): void {
this.setQuickLookPanelOpenState(false);

this.exportNotificationHeader = this.exportInProgressHeader;
this.exportNotificationMessage = "Downloading " + svcName + "...";
this.exportNotificationType = NotificationType.INFO;
this.exportNotificationVisible = true;
this.logger.log("[DataservicesPageComponent] Downloading Dataservice: " + svcName);
const self = this;
this.dataserviceService
.downloadDataservice(svcName)
.subscribe(
(wasSuccess) => {
self.exportNotificationHeader = this.downloadSuccessHeader;
self.exportNotificationMessage = " " + svcName + " was downloaded successfully!";
self.exportNotificationType = NotificationType.SUCCESS;
this.logger.log("[DataservicesPageComponent] Download Dataservice was successful");
},
(error) => {
self.exportNotificationHeader = this.downloadFailedHeader;
self.exportNotificationMessage = " Failed to downlaod dataservice " + svcName;
self.exportNotificationType = NotificationType.DANGER;
this.logger.error("[DataservicesPageComponent] Download dataservice " + svcName + " failed.");
}
);
}

public onPublish(svcName: string): void {
this.setQuickLookPanelOpenState(false);

Expand Down
96 changes: 96 additions & 0 deletions ngapp/src/app/dataservices/shared/dataservice.service.ts
Expand Up @@ -35,6 +35,7 @@ import { Observable } from "rxjs/Observable";
import { ReplaySubject } from "rxjs/ReplaySubject";
import { Subject } from "rxjs/Subject";
import { Subscription } from "rxjs/Subscription";
import { saveAs } from 'file-saver/FileSaver';

@Injectable()
export class DataserviceService extends ApiService {
Expand Down Expand Up @@ -357,6 +358,101 @@ export class DataserviceService extends ApiService {
.flatMap((res) => this.createDataserviceForSingleSourceTables(dataservice, sourceTables));
}

/**
* Converts a base64 data string into a blob for use with the FileSaver library
* Acknowledgement to
* http://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript
*/
private b64toBlob(b64Data: string, contentType: string): Blob {
contentType = contentType || '';
let sliceSize = 512;

//
// Decodes the base64 string back into binary data byte characters
//
let byteCharacters = atob(b64Data);
let byteArrays = [];

//
// Each character's code point (charCode) will be the value of the byte.
// Can create an array of byte values by applying this using the .charCodeAt
// method for each character in the string.
//
// The performance can be improved a little by processing the byteCharacters
// in smaller slices, rather than all at once. Rough testing indicates 512 bytes
// seems to be a good slice size.
//
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
let slice = byteCharacters.slice(offset, offset + sliceSize);

let byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}

//
// Convert the array of byte values into a real typed byte array
// by passing it to the Uint8Array constructor.
//
let byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}

//
// Convert to a Blob by wrapping it in an array passing it to the Blob constructor.
//
let blob = new Blob(byteArrays, {
type: contentType
});

return blob;
}

/**
* Download a dataservice as a jar archive
* @param {string} dataserviceName the dataservice name
* @returns {Observable<boolean>}
*/
public downloadDataservice(dataserviceName: string): Observable<boolean> {
// The payload for the rest call
const payload = {
"storageType": "file",
"dataPath": this.getKomodoUserWorkspacePath() + "/" + dataserviceName,
"parameters": {}
};

const url = environment.komodoImportExportUrl + "/" + DataservicesConstants.dataservicesExport;

return this.http
.post(url, payload, this.getAuthRequestOptions())
.map((response) => {
let status = response.json();
console.log("Response: " + response);

if (! status.downloadable) {
throw new Error(payload.dataPath + " is not downloadable");
}

if (! status.content) {
throw new Error(payload.dataPath + " has no content");
}

const name = status.Name || dataserviceName;
const fileType = status.type || 'data';
const enc = status.content;

const contentType = fileType === "zip" ? 'application/zip' : 'text/plain;charset=utf-8';
const dataBlob = this.b64toBlob(enc, contentType);

const fileExt = ( fileType == "-vdb.xml" || fileType == "-connection.xml" ) ? fileType : "." + fileType;

saveAs(dataBlob, name + fileExt);

return response.ok;
})
.catch( ( error ) => this.handleError( error ) );
}

/**
* Export a dataservice to a git repository
* @param {string} dataserviceName the dataservice name
Expand Down

0 comments on commit 87ef7ae

Please sign in to comment.