From ed7923f4b8bd93ed16d0edbf4eeb5cd1caa96952 Mon Sep 17 00:00:00 2001 From: Nick Byrne Date: Sat, 19 Oct 2024 20:29:00 -0300 Subject: [PATCH] Move queryParameters and baseUrl to properties to allow patching settings --- schema/commands.json | 24 +++-- src/index.ts | 247 ++++++++++++++++++++++++++++--------------- 2 files changed, 176 insertions(+), 95 deletions(-) diff --git a/schema/commands.json b/schema/commands.json index 7785976..ef8857f 100644 --- a/schema/commands.json +++ b/schema/commands.json @@ -9,8 +9,7 @@ { "command": "jhub-apps:deploy-app", "args": { - "origin": "main-menu", - "queryParameters": { "headless": "true" } + "origin": "main-menu" } } ] @@ -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 @@ -34,8 +32,7 @@ "name": "deploy-app", "command": "jhub-apps:deploy-app", "args": { - "origin": "toolbar", - "queryParameters": { "headless": "true" } + "origin": "toolbar" }, "label": "Deploy App" } @@ -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 } diff --git a/src/index.ts b/src/index.ts index 28d371d..179db80 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,25 @@ 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. @@ -15,12 +33,7 @@ namespace CommandIDs { 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. */ @@ -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>; +} + +interface IDeployAppWidgetArgs extends IDeployAppBaseConfig {} + interface IPathWidget { /** * The context object associated with the widget. @@ -48,6 +73,12 @@ interface IPathWidget { }; } +function applySettings(setting: ISettingRegistry.ISettings): void { + runtimeSettings.queryParameters = setting.get('queryParameters') + .composite as Record; + runtimeSettings.baseUrl = setting.get('baseUrl').composite as string; +} + const hasContextPath = (widget: any): widget is IPathWidget => { return widget && widget.context && typeof widget.context.path === 'string'; }; @@ -68,7 +99,7 @@ function coerceBooleanString(value: any): 'true' | 'false' { return 'true'; } -export function buildQueryString(params: Record): string { +function buildQueryString(params: Record): string { return Object.entries(params) .map( ([key, value]) => @@ -77,93 +108,133 @@ export function buildQueryString(params: Record): 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 | 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 = { - 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 | 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>({ 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, @@ -187,12 +258,12 @@ class DeployAppWidget extends Widget { private _queryParameters: Record; 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';