Skip to content

Commit 6c85478

Browse files
Add manager for lab extension
This manager will be added to running sessions widget so that user can view and terminate running server proxy apps. Proxy app metadata will be shown when we hover over proxy app name.
1 parent 9833b19 commit 6c85478

File tree

4 files changed

+390
-1
lines changed

4 files changed

+390
-1
lines changed

labextension/src/index.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,21 @@ import {
55
} from "@jupyterlab/application";
66
import { ILauncher } from "@jupyterlab/launcher";
77
import { PageConfig } from "@jupyterlab/coreutils";
8+
import { IRunningSessionManagers, IRunningSessions } from '@jupyterlab/running';
89
import { IFrame, MainAreaWidget, WidgetTracker } from "@jupyterlab/apputils";
10+
import { LabIcon } from '@jupyterlab/ui-components';
11+
import { ServerProxyManager } from './manager';
12+
import { IModel as IServerProxyModel } from './serverproxy';
13+
import serverProxyAppSvgstr from '../style/icons/proxy.svg';
14+
15+
export const ServerProxyAppIcon = new LabIcon({
16+
name: 'server-proxy:proxyAppIcon',
17+
svgstr: serverProxyAppSvgstr
18+
});
19+
20+
namespace CommandIDs {
21+
export const open = 'running-server-proxy:open';
22+
}
923

1024
function newServerProxyWidget(
1125
id: string,
@@ -32,6 +46,53 @@ function newServerProxyWidget(
3246
return widget;
3347
}
3448

49+
/**
50+
* This function adds the active server proxy applications to running sessions
51+
* so that user can track currently running applications via server proxy.
52+
* User can shut down the applications as well to restart them in future
53+
*
54+
*/
55+
function addRunningSessionManager(
56+
managers: IRunningSessionManagers,
57+
app: JupyterFrontEnd,
58+
manager: ServerProxyManager
59+
): void {
60+
managers.add({
61+
name: 'Server Proxy Apps',
62+
running: () =>
63+
Array.from(manager.running()).map(
64+
model => new RunningServerProxyApp(model)
65+
),
66+
shutdownAll: () => manager.shutdownAll(),
67+
refreshRunning: () => manager.refreshRunning(),
68+
runningChanged: manager.runningChanged,
69+
shutdownAllConfirmationText: 'Are you sure you want to close all server proxy applications?'
70+
});
71+
72+
class RunningServerProxyApp implements IRunningSessions.IRunningItem {
73+
constructor(model: IServerProxyModel) {
74+
this._model = model;
75+
}
76+
open(): void {
77+
app.commands.execute(CommandIDs.open, { sp: this._model });
78+
}
79+
icon(): LabIcon {
80+
return ServerProxyAppIcon;
81+
}
82+
label(): string {
83+
return `${this._model.name}`;
84+
}
85+
labelTitle(): string {
86+
return `cmd: ${this._model.cmd}\nport: ${this._model.port}\nmanaged: ${this._model.managed}`;
87+
}
88+
shutdown(): Promise<void> {
89+
return manager.shutdown(this._model.name);
90+
}
91+
92+
private _model: IServerProxyModel;
93+
}
94+
}
95+
3596
/**
3697
* The activate function is registered to be called on activation of the
3798
* jupyterlab extension.
@@ -42,10 +103,11 @@ async function activate(
42103
app: JupyterFrontEnd,
43104
launcher: ILauncher,
44105
restorer: ILayoutRestorer,
106+
sessions: IRunningSessionManagers | null
45107
): Promise<void> {
46108
// Fetch configured server processes from {base_url}/server-proxy/servers-info
47109
const response = await fetch(
48-
PageConfig.getBaseUrl() + "server-proxy/servers-info",
110+
PageConfig.getBaseUrl() + "api/server-proxy/servers-info",
49111
);
50112
if (!response.ok) {
51113
console.log(
@@ -76,6 +138,13 @@ async function activate(
76138
}
77139

78140
const { commands, shell } = app;
141+
142+
// Add server proxy session manager to running sessions
143+
if (sessions) {
144+
let manager = new ServerProxyManager();
145+
addRunningSessionManager(sessions, app, manager);
146+
}
147+
79148
commands.addCommand(command, {
80149
label: (args) => args["title"] as string,
81150
execute: (args) => {
@@ -105,6 +174,15 @@ async function activate(
105174
},
106175
});
107176

177+
commands.addCommand(CommandIDs.open, {
178+
execute: args => {
179+
const model = args['sp'] as IServerProxyModel;
180+
const url = PageConfig.getBaseUrl() + model.url;
181+
window.open(url, '_blank');
182+
return;
183+
}
184+
});
185+
108186
for (let server_process of data.server_processes) {
109187
if (!server_process.launcher_entry.enabled) {
110188
continue;

labextension/src/manager.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { Signal, ISignal } from '@lumino/signaling';
2+
import { ServerConnection } from '@jupyterlab/services';
3+
import { listRunning, shutdown } from './restapi';
4+
import * as ServerProxyApp from './serverproxy';
5+
6+
/**
7+
* A server proxy manager.
8+
*/
9+
export class ServerProxyManager implements ServerProxyApp.IManager {
10+
/**
11+
* Construct a new server proxy manager.
12+
*/
13+
constructor(options: ServerProxyManager.IOptions = {}) {
14+
this.serverSettings = options.serverSettings || ServerConnection.makeSettings();
15+
this._refreshTimer = (setInterval as any)(() => {
16+
if (typeof document !== 'undefined' && document.hidden) {
17+
return;
18+
}
19+
this._refreshRunning();
20+
}, 10000);
21+
}
22+
23+
/**
24+
* The server settings of the manager.
25+
*/
26+
readonly serverSettings: ServerConnection.ISettings;
27+
28+
/**
29+
* A signal emitted when the running server proxies change.
30+
*/
31+
get runningChanged(): ISignal<this, ServerProxyApp.IModel[]> {
32+
return this._runningChanged;
33+
}
34+
35+
/**
36+
* A signal emitted when there is a connection failure.
37+
*/
38+
get connectionFailure(): ISignal<this, Error> {
39+
return this._connectionFailure;
40+
}
41+
42+
/**
43+
* Test whether the delegate has been disposed.
44+
*/
45+
get isDisposed(): boolean {
46+
return this._isDisposed;
47+
}
48+
49+
/**
50+
* Dispose of the resources used by the manager.
51+
*/
52+
dispose(): void {
53+
if (this.isDisposed) {
54+
return;
55+
}
56+
this._isDisposed = true;
57+
clearInterval(this._refreshTimer);
58+
Signal.clearData(this);
59+
}
60+
61+
/**
62+
* Create an iterator over the most recent running proxy apps.
63+
*
64+
* @returns A new iterator over the running proxy apps.
65+
*/
66+
running(): IterableIterator<ServerProxyApp.IModel> {
67+
return this._models[Symbol.iterator]();
68+
}
69+
70+
/**
71+
* Shut down a server proxy app by name.
72+
*/
73+
async shutdown(name: string): Promise<void> {
74+
await shutdown(name, this.serverSettings);
75+
await this.refreshRunning();
76+
}
77+
78+
/**
79+
* Shut down all server proxy apps.
80+
*
81+
* @returns A promise that resolves when all of the apps are shut down.
82+
*/
83+
async shutdownAll(): Promise<void> {
84+
// Update the list of models to make sure our list is current.
85+
await this.refreshRunning();
86+
87+
// Shut down all models.
88+
await Promise.all(
89+
this._names.map(name => shutdown(name, this.serverSettings))
90+
);
91+
92+
// Update the list of models to clear out our state.
93+
await this.refreshRunning();
94+
}
95+
96+
/**
97+
* Force a refresh of the running server proxy apps.
98+
*
99+
* @returns A promise that with the list of running proxy apps.
100+
*/
101+
async refreshRunning(): Promise<void> {
102+
return this._refreshRunning();
103+
}
104+
105+
/**
106+
* Refresh the running proxy apps.
107+
*/
108+
private async _refreshRunning(): Promise<void> {
109+
let models: ServerProxyApp.IModel[];
110+
try {
111+
models = await listRunning(this.serverSettings);
112+
} catch (err: any) {
113+
// Handle network errors, as well as cases where we are on a
114+
// JupyterHub and the server is not running. JupyterHub returns a
115+
// 503 (<2.0) or 424 (>2.0) in that case.
116+
if (
117+
err instanceof ServerConnection.NetworkError ||
118+
err.response?.status === 503 ||
119+
err.response?.status === 424
120+
) {
121+
this._connectionFailure.emit(err);
122+
}
123+
throw err;
124+
}
125+
126+
if (this.isDisposed) {
127+
return;
128+
}
129+
130+
const names = models.map(({ name }) => name).sort();
131+
if (names === this._names) {
132+
// Identical models list, so just return
133+
return;
134+
}
135+
136+
this._names = names;
137+
this._models = models;
138+
this._runningChanged.emit(this._models);
139+
}
140+
141+
private _names: string[] = [];
142+
private _models: ServerProxyApp.IModel[] = [];
143+
144+
private _isDisposed = false;
145+
private _refreshTimer = -1;
146+
private _runningChanged = new Signal<this, ServerProxyApp.IModel[]>(this);
147+
private _connectionFailure = new Signal<this, Error>(this);
148+
}
149+
150+
/**
151+
* The namespace for `BaseManager` class statics.
152+
*/
153+
export namespace ServerProxyManager {
154+
/**
155+
* The options used to initialize a SessionManager.
156+
*/
157+
export interface IOptions {
158+
/**
159+
* The server settings for the manager.
160+
*/
161+
serverSettings?: ServerConnection.ISettings;
162+
}
163+
}

labextension/src/restapi.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { URLExt } from '@jupyterlab/coreutils';
2+
import { ServerConnection } from '@jupyterlab/services';
3+
import { IModel } from './serverproxy'
4+
5+
/**
6+
* The url for the server proxy service.
7+
*/
8+
const SERVER_PROXY_SERVICE_URL = 'api/server-proxy';
9+
10+
/**
11+
* List the running server proxy apps.
12+
*
13+
* @param settings - The server settings to use.
14+
*
15+
* @returns A promise that resolves with the list of running session models.
16+
*/
17+
export async function listRunning(
18+
settings: ServerConnection.ISettings = ServerConnection.makeSettings()
19+
): Promise<IModel[]> {
20+
const url = URLExt.join(settings.baseUrl, SERVER_PROXY_SERVICE_URL);
21+
const response = await ServerConnection.makeRequest(url, {}, settings);
22+
if (response.status !== 200) {
23+
const err = await ServerConnection.ResponseError.create(response);
24+
throw err;
25+
}
26+
const data = await response.json();
27+
28+
if (!Array.isArray(data)) {
29+
throw new Error('Invalid server proxy list');
30+
}
31+
32+
return data;
33+
}
34+
35+
/**
36+
* Shut down a server proxy app by name.
37+
*
38+
* @param name - The name of the target server proxy app.
39+
*
40+
* @param settings - The server settings to use.
41+
*
42+
* @returns A promise that resolves when the app is shut down.
43+
*/
44+
export async function shutdown(
45+
name: string,
46+
settings: ServerConnection.ISettings = ServerConnection.makeSettings()
47+
): Promise<void> {
48+
const url = URLExt.join(settings.baseUrl, SERVER_PROXY_SERVICE_URL, name);
49+
const init = { method: 'DELETE' };
50+
const response = await ServerConnection.makeRequest(url, init, settings);
51+
if (response.status === 404) {
52+
const msg = `Server proxy "${name}" does not exist. Are you sure "${name}" is started by jupyter-server-proxy?`;
53+
console.warn(msg);
54+
} else if (response.status !== 204) {
55+
const err = await ServerConnection.ResponseError.create(response);
56+
throw err;
57+
}
58+
}

0 commit comments

Comments
 (0)