Skip to content

Commit

Permalink
Move queryParameters and baseUrl to properties to allow patching sett…
Browse files Browse the repository at this point in the history
…ings
  • Loading branch information
nenb committed Oct 21, 2024
1 parent 9d9e6a9 commit ed7923f
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 95 deletions.
24 changes: 17 additions & 7 deletions schema/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
{
"command": "jhub-apps:deploy-app",
"args": {
"origin": "main-menu",
"queryParameters": { "headless": "true" }
"origin": "main-menu"
}
}
]
Expand All @@ -20,8 +19,7 @@
{
"command": "jhub-apps:deploy-app",
"args": {
"origin": "context-menu",
"queryParameters": { "headless": "true" }
"origin": "context-menu"
},
"selector": ".jp-DirListing-item[data-isdir=\"false\"]",
"rank": 3
Expand All @@ -34,8 +32,7 @@
"name": "deploy-app",
"command": "jhub-apps:deploy-app",
"args": {
"origin": "toolbar",
"queryParameters": { "headless": "true" }
"origin": "toolbar"
},
"label": "Deploy App"
}
Expand All @@ -44,6 +41,19 @@
"title": "jupyterlab-jhub-apps",
"description": "jupyterlab-jhub-apps custom command settings.",
"type": "object",
"properties": {},
"properties": {
"baseUrl": {
"type": "string",
"title": "Base URL",
"description": "The base URL of the app deployment form.",
"default": "/services/japps/create-app"
},
"queryParameters": {
"type": "object",
"title": "Query Parameters",
"description": "Additional query parameters to be appended to the baseUrl. All keys and values must be strings.",
"default": { "headless": "true" }
}
},
"additionalProperties": false
}
247 changes: 159 additions & 88 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,33 @@ import { deployAppIcon } from './icons';
import { WidgetTracker, IFrame, MainAreaWidget } from '@jupyterlab/apputils';
import { Widget } from '@lumino/widgets';

import { ISettingRegistry } from '@jupyterlab/settingregistry';

const DEFAULT_BASE_URL = '/services/japps/create-app';

const PLUGIN_ID = 'jupyterlab-jhub-apps:commands';

/**
* Settings for the runtime configuration of the application.
* These are read from the settings registry and updated any
* time a user changes their settings.
*/
const runtimeSettings = {
queryParameters: {} as {
headless?: string;
filepath?: string;
},
baseUrl: DEFAULT_BASE_URL
};

namespace CommandIDs {
/**
* Opens the URL for deploying an app with pre-populated fields.
*/
export const deployApp = 'jhub-apps:deploy-app';
}

interface IDeployAppArgs {
/**
* The origin of the command e.g. main-menu, context-menu, etc.,
* which determines the icon and label to display.
*/
origin?: string;
interface IDeployAppBaseConfig {
/**
* The base URL of the app deployment form.
*/
Expand All @@ -34,6 +47,18 @@ interface IDeployAppArgs {
};
}

interface IDeployAppConfig extends IDeployAppBaseConfig {
/**
* The origin of the command e.g. main-menu, context-menu, etc.,
* which determines the icon and label to display.
*/
origin?: string;
IShell?: JupyterFrontEnd.IShell;
widgetTracker?: WidgetTracker<MainAreaWidget<DeployAppWidget>>;
}

interface IDeployAppWidgetArgs extends IDeployAppBaseConfig {}

interface IPathWidget {
/**
* The context object associated with the widget.
Expand All @@ -48,6 +73,12 @@ interface IPathWidget {
};
}

function applySettings(setting: ISettingRegistry.ISettings): void {
runtimeSettings.queryParameters = setting.get('queryParameters')
.composite as Record<string, string>;
runtimeSettings.baseUrl = setting.get('baseUrl').composite as string;
}

const hasContextPath = (widget: any): widget is IPathWidget => {
return widget && widget.context && typeof widget.context.path === 'string';
};
Expand All @@ -68,7 +99,7 @@ function coerceBooleanString(value: any): 'true' | 'false' {
return 'true';
}

export function buildQueryString(params: Record<string, string>): string {
function buildQueryString(params: Record<string, string>): string {
return Object.entries(params)
.map(
([key, value]) =>
Expand All @@ -77,93 +108,133 @@ export function buildQueryString(params: Record<string, string>): string {
.join('&');
}

const calculateIcon = (args: IDeployAppConfig) => {
switch (args.origin) {
case 'main-menu':
return undefined;
case 'context-menu':
case 'toolbar':
case undefined:
default:
return deployAppIcon;
}
};

const openAppForm = ({
origin,
baseUrl,
IShell,
widgetTracker,
queryParameters = {}
}: IDeployAppConfig): void => {
if (!IShell || !widgetTracker) {
console.warn(
'IShell and widgetTracker not defined. These are required to open the deploy app form.'
);
return;
}

const currentWidget = IShell.currentWidget;

const filepath = hasContextPath(currentWidget)
? currentWidget.context.path
: '';

const selectedFilepath =
queryParameters.filepath !== undefined
? queryParameters.filepath
: filepath;

const headless = coerceBooleanString(queryParameters.headless);

const updatedQueryParameters = {
...queryParameters,
headless: headless,
filepath: selectedFilepath
};

let mainAreaWidget: MainAreaWidget<DeployAppWidget> | undefined;

if (headless === 'true') {
try {
if (!mainAreaWidget || mainAreaWidget.isDisposed) {
const content = new DeployAppWidget({
baseUrl: baseUrl,
queryParameters: updatedQueryParameters
});
mainAreaWidget = new MainAreaWidget({ content });
}

if (!widgetTracker.has(mainAreaWidget)) {
widgetTracker.add(mainAreaWidget);
}

// non-empty origin implies user has called command e.g. main-menu click.
// guards against (what appears to be) command execution from tracker
// on session restore.
if (origin && !mainAreaWidget.isAttached) {
IShell.add(mainAreaWidget, 'main');
}
} catch (error) {
console.warn(`Error opening in headless mode: ${error}`);
}
} else {
try {
// no restore logic required here as opening in a separate window
const completeUrl = `${baseUrl}?${buildQueryString(updatedQueryParameters)}`;
window.open(completeUrl, '_blank', 'noopener,noreferrer');
} catch (error) {
console.warn(`Error opening in window: ${error}`);
}
}
};

const jhubAppsPlugin: JupyterFrontEndPlugin<void> = {
id: 'jupyterlab-jhub-apps:commands',
id: PLUGIN_ID,
description: 'Adds additional commands used by jhub-apps.',
autoStart: true,
requires: [ISettingRegistry],
optional: [ILayoutRestorer],
activate: (app: JupyterFrontEnd, restorer: ILayoutRestorer | null) => {
const calculateIcon = (args: IDeployAppArgs) => {
switch (args.origin) {
case 'main-menu':
return undefined;
case 'context-menu':
case 'toolbar':
case undefined:
default:
return deployAppIcon;
}
};
activate: async (
app: JupyterFrontEnd,
settingsRegistry: ISettingRegistry,
restorer: ILayoutRestorer | null
) => {
const { commands, shell } = app;

app.commands.addCommand(CommandIDs.deployApp, {
execute: ({
origin,
baseUrl = DEFAULT_BASE_URL,
queryParameters = {}
}: IDeployAppArgs) => {
const currentWidget = app.shell.currentWidget;
const filepath = hasContextPath(currentWidget)
? currentWidget.context.path
: '';

const selectedFilepath =
queryParameters.filepath !== undefined
? queryParameters.filepath
: filepath;

const headless = coerceBooleanString(queryParameters.headless);

const updatedQueryParameters = {
...queryParameters,
headless: headless,
filepath: selectedFilepath
};

let mainAreaWidget: MainAreaWidget<DeployAppWidget> | undefined;
if (headless === 'true') {
try {
if (!mainAreaWidget || mainAreaWidget.isDisposed) {
const content = new DeployAppWidget({
origin: origin,
baseUrl: baseUrl,
queryParameters: updatedQueryParameters
});
mainAreaWidget = new MainAreaWidget({ content });
}

if (!tracker.has(mainAreaWidget)) {
tracker.add(mainAreaWidget);
}

// non-empty origin implies user has called command e.g. main-menu click.
// guards against (what appears to be) command execution from tracker
// on session restore.
if (origin) {
if (!mainAreaWidget.isAttached) {
app.shell.add(mainAreaWidget, 'main');
}
}
} catch (error) {
console.warn(`Error opening in headless mode: ${error}`);
}
} else {
try {
// no restore logic required here as opening in a separate window
const completeUrl = `${baseUrl}?${buildQueryString(updatedQueryParameters)}`;
window.open(completeUrl, '_blank', 'noopener,noreferrer');
} catch (error) {
console.warn(`Error opening in window: ${error}`);
}
}
},
label: 'Deploy App',
icon: calculateIcon
});
try {
const baseSettings = await settingsRegistry.load(PLUGIN_ID);
applySettings(baseSettings);
baseSettings.changed.connect(applySettings);
} catch (error) {
console.warn(`Failed to load settings for ${PLUGIN_ID}:`, error);
return;
}

const tracker = new WidgetTracker<MainAreaWidget<DeployAppWidget>>({
namespace: 'jhub-apps'
});

commands.addCommand(CommandIDs.deployApp, {
label: 'Deploy App',
icon: calculateIcon,
execute: ({
origin = undefined,
queryParameters = runtimeSettings.queryParameters,
baseUrl = runtimeSettings.baseUrl,
IShell = shell,
widgetTracker = tracker
}: IDeployAppConfig) => {
openAppForm({
origin,
queryParameters,
baseUrl,
IShell,
widgetTracker
});
}
});

if (restorer) {
restorer.restore(tracker, {
command: CommandIDs.deployApp,
Expand All @@ -187,12 +258,12 @@ class DeployAppWidget extends Widget {
private _queryParameters: Record<string, string>;

constructor({
queryParameters = {},
baseUrl = DEFAULT_BASE_URL
}: IDeployAppArgs) {
baseUrl = DEFAULT_BASE_URL,
queryParameters = {}
}: IDeployAppWidgetArgs) {
super();
this._queryParameters = queryParameters;
this._baseUrl = baseUrl;
this._queryParameters = queryParameters;

this.addClass('jp-deploy-app-widget');
this.id = 'deploy-app-jupyterlab';
Expand Down

0 comments on commit ed7923f

Please sign in to comment.