Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat: add edu-applauncher page and component #964

Merged
merged 5 commits into from
Apr 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/backend-ai-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ const loadPage = (page, params: Record<string, unknown> = {}) => (dispatch) => {
case 'import':
import('./components/backend-ai-import-view.js');
break;
case 'edu-applauncher':
import('./components/backend-ai-edu-applauncher.js');
break;
case 'unauthorized':
import('./components/backend-ai-permission-denied-view.js');
break;
Expand Down
213 changes: 213 additions & 0 deletions src/components/backend-ai-edu-applauncher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/**
u@license
Copyright (c) 2015-2021 Lablup Inc. All rights reserved.
*/
import {get as _text, translate as _t, translateUnsafeHTML as _tr} from 'lit-translate';
import {css, CSSResultArray, CSSResultOrNative, customElement, html, property} from 'lit-element';
import {BackendAIPage} from './backend-ai-page';

import {BackendAiStyles} from './backend-ai-general-styles';
import {
IronFlex,
IronFlexAlignment,
IronFlexFactors,
IronPositioning
} from '../plastics/layout/iron-flex-layout-classes';

import 'weightless/button';
import 'weightless/icon';
import 'weightless/card';

import '@material/mwc-icon/mwc-icon';

import {default as PainKiller} from './backend-ai-painkiller';
import './backend-ai-app-launcher';
import './lablup-activity-panel';
import './lablup-loading-spinner';

/**
Backend.AI Education App Launcher.

Available url parameters:
- app: str = 'jupyter' (app name to launch)
- scaling_group: str = 'default' (scaling group to create a new session)

Example:

<backend-ai-edu-applauncher page="class" id="edu-applauncher" ?active="${0}">
... content ...
</backend-ai-edu-applauncher>

@group Backend.AI Web UI
@element backend-ai-edu-applauncher
*/

@customElement('backend-ai-edu-applauncher')
export default class BackendAiEduApplauncher extends BackendAIPage {
@property({type: Object}) notification = Object();

static get styles(): CSSResultOrNative | CSSResultArray {
return [
BackendAiStyles,
IronFlex,
IronFlexAlignment,
IronFlexFactors,
IronPositioning,
// language=CSS
css`
`
];
}

render() {
// language=HTML
return html`
<backend-ai-app-launcher id="app-launcher"></backend-ai-app-launcher>
`;
}

firstUpdated() {
this.notification = globalThis.lablupNotification;
if (typeof globalThis.backendaiclient === 'undefined' || globalThis.backendaiclient === null) {
document.addEventListener('backend-ai-connected', () => {
this._createEduSession();
}, true);
} else { // already connected
}
}

async _viewStateChanged(active) {
await this.updateComplete;
if (!this.active) {
return;
}
}

/**
* Randomly generate session ID
*
* @return {string} Generated session ID
* */
generateSessionId() {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 8; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text + '-session';
}

async _createEduSession() {
// Query current user's compute session in the current group.
const fields = [
'session_id', 'name', 'access_key', 'status', 'status_info', 'service_ports', 'mounts',
];
const statuses = ['RUNNING', 'RESTARTING', 'TERMINATING', 'PENDING', 'PREPARING', 'PULLING'].join(',');
const accessKey = globalThis.backendaiclient._config.accessKey;
// NOTE: There is no way to change the default group.
// This API should be used when there is only one group, 'default'.
const groupId = globalThis.backendaiclient.current_group_id();
const sessions = await globalThis.backendaiclient.computeSession.list(
fields, statuses, accessKey, 30, 0, groupId
);

// URL Parameter parsing.
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const requestedApp = urlParams.get('app') || 'jupyter';
const scalingGroup = urlParams.get('scaling_group') || 'default';

// Create or select an existing compute session before lauching app.
let sessionId;
if (sessions.compute_session_list.total_count > 0) {
console.log('Reusing an existing session ...');
const sessionStatus = sessions.compute_session_list.items[0].status;
if (sessionStatus !== 'RUNNING') {
this.notification.text = `Your session is ${sessionStatus}. Please reload after some time.`;
this.notification.show(true);
return;
}
let sess = null;
for (let i = 0; i < sessions.compute_session_list.items.length; i++) {
const _sess = sessions.compute_session_list.items[i];
const servicePorts = JSON.parse(_sess.service_ports || '{}');
const services = servicePorts.map((s) => s.name);
if (services.includes(requestedApp)) {
sess = _sess;
break;
}
}
if (!sess) {
this.notification.text = `No existing session can launch ${requestedApp}`;
this.notification.show(true);
return;
}
sessionId = sess.session_id;
} else { // no existing compute session. create one.
console.log('Creating a new session ...');
let sessionTemplates = await globalThis.backendaiclient.sessionTemplate.list(false, groupId);
// Assume that session templates' name match requsetedApp name.
sessionTemplates = sessionTemplates.filter((t) => t.name === requestedApp);
if (sessionTemplates.length < 1) {
this.notification.text = 'No appropriate session templates';
this.notification.show(true);
return;
}
const templateId = sessionTemplates[0].id; // NOTE: use the first template. will it be okay?
try {
const resources = {
scaling_group: scalingGroup,
mounts: [],
};
const response = await globalThis.backendaiclient.createSessionFromTemplate(templateId, null, null, resources);
sessionId = response.sessionId;
} catch (err) {
console.error(err);
if (err && err.message) {
if ('statusCode' in err && err.statusCode === 408) {
this.notification.text = 'Session is still in preparing. Reload after a while.';
} else {
if (err.description) {
this.notification.text = PainKiller.relieve(err.description);
} else {
this.notification.text = PainKiller.relieve(err.message);
}
}
this.notification.detail = err.message;
this.notification.show(true, err);
} else if (err && err.title) {
this.notification.text = PainKiller.relieve(err.title);
this.notification.show(true, err);
}
}
}

// Launch app.
// TODO: launch 'jupyterlab' if the browser is not IE.
this._openServiceApp(sessionId, requestedApp);
}

async _openServiceApp(sessionId, appName) {
const appLauncher = this.shadowRoot.querySelector('#app-launcher');
appLauncher.indicator = await globalThis.lablupIndicator.start();
appLauncher._open_wsproxy(sessionId, appName, null, null)
.then(async (resp) => {
console.log(resp);
if (resp.url) {
await appLauncher._connectToProxyWorker(resp.url, '');
appLauncher.indicator.set(100, _text('session.applauncher.Prepared'));
setTimeout(() => {
// globalThis.open(resp.url, '_self');
globalThis.open(resp.url);
});
} else {
}
});
}
}

declare global {
interface HTMLElementTagNameMap {
'backend-ai-edu-applauncher': BackendAiEduApplauncher;
}
}
1 change: 1 addition & 0 deletions src/components/backend-ai-webui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1488,6 +1488,7 @@ export default class BackendAIWebUI extends connect(store)(LitElement) {
<backend-ai-statistics-view class="page" name="statistics" ?active="${this._page === 'statistics'}"><mwc-circular-progress indeterminate></mwc-circular-progress></backend-ai-statistics-view>
<backend-ai-email-verification-view class="page" name="email-verification" ?active="${this._page === 'verify-email'}"><mwc-circular-progress indeterminate></mwc-circular-progress></backend-ai-email-verification-view>
<backend-ai-change-forgot-password-view class="page" name="change-forgot-password" ?active="${this._page === 'change-password'}"><mwc-circular-progress indeterminate></mwc-circular-progress></backend-ai-change-forgot-password-view>
<backend-ai-edu-applauncher class="page" name="edu-applauncher" ?active="${this._page === 'edu-applauncher'}"><mwc-circular-progress indeterminate></mwc-circular-progress></backend-ai-edu-applauncher>
<backend-ai-error-view class="page" name="error" ?active="${this._page === 'error'}"><mwc-circular-progress indeterminate></mwc-circular-progress></backend-ai-error-view>
<backend-ai-permission-denied-view class="page" name="unauthorized" ?active="${this._page === 'unauthorized'}"><mwc-circular-progress indeterminate></mwc-circular-progress></backend-ai-permission-denied-view>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/components/lablup-activity-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,8 @@ export default class LablupActivityPanel extends LitElement {
this.shadowRoot.querySelector('div.card > h4').style.marginBottom = '0';
}
if (this.height > 0) {
this.height == 130 ?
this.shadowRoot.querySelector('div.card').style.height = "fit-content" :
this.height == 130 ?
this.shadowRoot.querySelector('div.card').style.height = 'fit-content' :
this.shadowRoot.querySelector('div.card').style.height = this.height + 'px';
}
if (this.noheader === true) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/backend.ai-client-es6.js

Large diffs are not rendered by default.

Loading