blob: b2f62311fb5891738eb75b8a6183b18833e955c5 [file] [log] [blame]
/*
* Copyright 2018-present Open Networking Foundation
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, OnInit, OnDestroy, Inject } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { DialogService } from '../../../fw/layer/dialog.service';
import { FnService } from '../../../fw/util/fn.service';
import { IconService } from '../../../fw/svg/icon.service';
import { KeyService } from '../../../fw/util/key.service';
import { LionService } from '../../../fw/util/lion.service';
import { LoadingService } from '../../../fw/layer/loading.service';
import { LogService } from '../../../log.service';
import { TableBaseImpl, TableResponse, TableFilter, SortParams, SortDir } from '../../../fw/widget/table.base';
import { UrlFnService } from '../../../fw/remote/urlfn.service';
import { WebSocketService } from '../../../fw/remote/websocket.service';
import { TableFilterPipe } from '../../../fw/widget/tablefilter.pipe';
const INSTALLED = 'INSTALLED';
const ACTIVE = 'ACTIVE';
const APPMGMTREQ = 'appManagementRequest';
const DETAILSREQ = 'appDetailsRequest';
const FILEUPLOADURL = 'upload';
const FILEDOWNLOADURL = 'download';
const ACTIVATEOPTION = '?activate=true';
const DRAGDROPMSG1 = 'Drag and drop one file at a time';
const DRAGDROPMSGEXT = 'Only files ending in .oar can be dropped';
/** Prefix to access the REST service for applications */
export const APPURLPREFIX = '../../ui/rs/applications/'; // TODO: This is a hack to work off GUIv1 URL
/** Suffix to access the icon of the application - gives back an image */
export const ICONURLSUFFIX = '/icon';
const downloadSuffix = '/download';
const dialogId = 'app-dialog';
const dialogOpts = {
edge: 'right',
width: 400,
};
const strongWarning = {
'org.onosproject.drivers': true,
};
const propOrder = ['id', 'state', 'category', 'version', 'origin', 'role'];
/**
* Model of the data returned through the Web Socket about apps.
*/
interface AppTableResponse extends TableResponse {
apps: App[];
}
/**
* Model of the data returned through Web Socket for a single App
*/
export interface App {
category: string;
desc: string;
features: string[];
icon: string;
id: string;
origin: string;
permissions: string[];
readme: string;
required_apps: string[];
role: string;
state: string;
title: string;
url: string;
version: string;
_iconid_state: string;
}
export enum AppAction {
NONE = 0,
ACTIVATE = 1,
DEACTIVATE = 2,
UNINSTALL = 3,
}
/**
* Model of the Control Button
*/
interface CtrlBtnState {
installed: boolean;
selection: string;
active: boolean;
}
/**
* ONOS GUI -- Apps View Component extends TableBaseImpl
*/
@Component({
selector: 'onos-apps',
templateUrl: './apps.component.html',
styleUrls: [
'./apps.component.css', './apps.theme.css',
'../../../fw/widget/table.css', '../../../fw/widget/table.theme.css'
]
})
export class AppsComponent extends TableBaseImpl implements OnInit, OnDestroy {
// deferred localization strings
lionFn; // Function
warnDeactivate: string;
warnOwnRisk: string;
ctrlBtnState: CtrlBtnState;
appFile: any;
uploadTip: string;
activateTip: string;
deactivateTip: string;
uninstallTip: string;
downloadTip: string;
alertMsg: string;
AppActionEnum: any = AppAction;
appAction: AppAction = AppAction.NONE;
confirmMsg: string = '';
strongWarning: string = '';
constructor(
protected fs: FnService,
private ds: DialogService,
private is: IconService,
private ks: KeyService,
private lion: LionService,
protected ls: LoadingService,
protected log: LogService,
private ufs: UrlFnService,
protected wss: WebSocketService,
@Inject('Window') private window: Window,
private httpClient: HttpClient
) {
super(fs, null, log, wss, 'app');
this.responseCallback = this.appResponseCb;
this.parentSelCb = this.rowSelection;
// pre-populate sort so active apps are at the top of the list
this.sortParams = {
firstCol: 'state',
firstDir: SortDir.desc,
secondCol: 'title',
secondDir: SortDir.asc,
};
// We want doLion() to be called only after the Lion
// service is populated (from the WebSocket)
// If lion is not ready we make do with a dummy function
// As soon a lion gets loaded this function will be replaced with
// the real thing
if (this.lion.ubercache.length === 0) {
this.lionFn = this.dummyLion;
this.lion.loadCbs.set('apps', () => this.doLion());
} else {
this.doLion();
}
this.ctrlBtnState = <CtrlBtnState>{
installed: false,
active: false
};
}
/**
* Initialize querying the WebSocket for App table details
*/
ngOnInit() {
this.init();
}
/**
* Stop sending queries to WebSocket
*/
ngOnDestroy() {
this.lion.loadCbs.delete('apps');
this.destroy();
this.log.debug('AppComponent destroyed');
}
/**
* The callback called when App data returns from WSS
*/
appResponseCb(data: AppTableResponse) {
this.log.debug('App response received for ', data.apps.length, 'apps');
}
/**
* called when a row is selected - sets the state of control icons
*/
rowSelection(event: any, selRow: any) {
this.ctrlBtnState.installed = this.selId && selRow && selRow.state === INSTALLED;
this.ctrlBtnState.active = this.selId && selRow && selRow.state === ACTIVE;
this.ctrlBtnState.selection = this.selId;
this.log.debug('Row ', this.selId, 'selected', this.ctrlBtnState);
}
/**
* Perform one of the app actions - activate, deactivate or uninstall
* Raises a dialog which calls back the dOk() below
*/
confirmAction(action: AppAction): void {
this.appAction = action;
const appActionLc = (<string>AppAction[this.appAction]).toLowerCase();
this.confirmMsg = this.lionFn(appActionLc) + ' ' + this.selId;
if (strongWarning[this.selId]) {
this.strongWarning = this.warnDeactivate + '\n' + this.warnOwnRisk;
}
this.log.debug('Initiating', this.appAction, 'of', this.selId);
}
/**
* Callback when the Confirm dialog is shown and a choice is made
*/
dOk(choice: boolean) {
const appActionLc = (<string>AppAction[this.appAction]).toLowerCase();
if (choice) {
this.log.debug('Confirmed', appActionLc, 'on', this.selId);
this.wss.sendEvent(APPMGMTREQ, {
action: appActionLc,
name: this.selId,
sortCol: this.sortParams.firstCol,
sortDir: SortDir[this.sortParams.firstDir],
});
if (this.appAction === AppAction.UNINSTALL) {
this.selId = '';
} else {
this.wss.sendEvent(DETAILSREQ, { id: this.selId });
}
} else {
this.log.debug('Cancelled', appActionLc, 'on', this.selId);
}
this.confirmMsg = '';
this.strongWarning = '';
}
downloadApp() {
if (this.ctrlBtnState.selection) {
(<any>this.window).location = APPURLPREFIX + this.selId + '/' + FILEDOWNLOADURL;
}
}
/**
* When the file is selected this fires
* It passes the file on to the server through a POST request
* If there is an error its logged and raised to the user through Flash Component
*/
fileEvent(event: any, activateImmediately?: boolean) {
this.log.debug('File event for', event.target.files[0]);
const formData = new FormData();
formData.append('file', event.target.files[0]);
let url = APPURLPREFIX + FILEUPLOADURL;
if (activateImmediately) {
url += ACTIVATEOPTION;
}
this.httpClient
.post<any>(APPURLPREFIX + FILEUPLOADURL, formData)
.subscribe(
data => this.log.debug(data),
err => {
this.log.warn(err.message);
this.alertMsg = err.message; // This will activate flash msg
}
);
}
/**
* When the upload button is clicked pass this on to the file input (hidden)
*/
triggerForm() {
document.getElementById('uploadFile')
.dispatchEvent(new MouseEvent('click'));
}
/**
* Read the LION bundle for App and set up the lionFn
*/
doLion() {
this.lionFn = this.lion.bundle('core.view.App');
this.warnDeactivate = this.lionFn('dlg_warn_deactivate');
this.warnOwnRisk = this.lionFn('dlg_warn_own_risk');
this.uploadTip = this.lionFn('tt_ctl_upload');
this.activateTip = this.lionFn('tt_ctl_activate');
this.deactivateTip = this.lionFn('tt_ctl_deactivate');
this.uninstallTip = this.lionFn('tt_ctl_uninstall');
this.downloadTip = this.lionFn('tt_ctl_download');
}
onDrop(event: DragEvent) {
event.preventDefault();
event.stopPropagation();
const dt = event.dataTransfer;
const droppedFiles = dt.files;
this.log.debug(droppedFiles.length, 'File(s) dropped');
if (droppedFiles.length !== 1) {
this.log.error(DRAGDROPMSG1, droppedFiles.length, 'were dropped');
this.alertMsg = DRAGDROPMSG1;
return;
} else if (droppedFiles[0].name.slice(droppedFiles[0].name.length - 4) !== '.oar') {
this.log.error(DRAGDROPMSGEXT, droppedFiles[0].name, 'rejected');
this.alertMsg = DRAGDROPMSGEXT;
return;
}
const fileEvent = {
target: {
files: droppedFiles
}
};
this.fileEvent(fileEvent, true);
}
onDragOver(evt) {
evt.preventDefault();
evt.stopPropagation();
}
onDragLeave(evt) {
evt.preventDefault();
evt.stopPropagation();
}
deselectRow(event) {
this.log.debug('Details panel close event');
this.selId = event;
this.ctrlBtnState = <CtrlBtnState>{
installed: undefined,
active: undefined
};
}
}