Skip to content

Commit

Permalink
Add new deploy app command (#1)
Browse files Browse the repository at this point in the history
* Add new deploy app command

* update readme

* Remove unnecessary notebook

* Remove unnecessary snapshots

* Update Playwright Snapshots

* remove unnecesary junit file

* Simplify icon logic

* Update icon to work across Jupyter themes

* Remove darwin files

* Remove class from <svg>

* Remove uneeded async

* Simplify switch logic

* Ensure kernel is in idle state

* Update tests

* Update Playwright Snapshots

* Add PR suggestions

* Change tests to hide kernel status indicator

* lint

* Update Playwright Snapshots

* Tidy unneeded asyncs

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
nenb and github-actions[bot] authored Sep 30, 2024
1 parent e2a2aa1 commit 0e0faf6
Show file tree
Hide file tree
Showing 15 changed files with 15,069 additions and 44 deletions.
2 changes: 1 addition & 1 deletion .copier-answers.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
_commit: v4.3.5
_src_path: https://github.com/jupyterlab/extension-template
author_email: nbyrne@quansight.com
author_email: internal-it@quansight.com
author_name: Nick Byrne
has_binder: true
has_settings: true
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,6 @@ dmypy.json

# virtualenv
.venv/

# JUnit test results
junit.xml
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
[![Github Actions Status](https://github.com/nebari-dev/jupyterlab-jhub-apps/workflows/Build/badge.svg)](https://github.com/nebari-dev/jupyterlab-jhub-apps/actions/workflows/build.yml)
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/nebari-dev/jupyterlab-jhub-apps/main?urlpath=lab)

Customizations for [jhub-apps](https://github.com/nebari-dev/jhub-apps).

Customizations for jhub-apps.
## Plugins

- `jhub-apps:deploy-app`: Adds a command to deploy an app from the current notebook. This command is available from the main menu and context menu, as well as a toolbar icon.

## Requirements

Expand Down
42 changes: 42 additions & 0 deletions schema/commands.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"jupyter.lab.menus": {
"main": [
{
"id": "jp-mainmenu-services",
"label": "Services",
"rank": 1000,
"items": [
{
"command": "jhub-apps:deploy-app",
"args": {
"origin": "main-menu"
}
}
]
}
],
"context": [
{
"command": "jhub-apps:deploy-app",
"args": {
"origin": "context-menu"
},
"selector": ".jp-DirListing-item[data-isdir=\"false\"]",
"rank": 3
}
]
},
"jupyter.lab.toolbars": {
"Notebook": [
{
"name": "deploy-app",
"command": "jhub-apps:deploy-app"
}
]
},
"title": "jupyterlab-jhub-apps",
"description": "jupyterlab-jhub-apps custom command settings.",
"type": "object",
"properties": {},
"additionalProperties": false
}
8 changes: 0 additions & 8 deletions schema/plugin.json

This file was deleted.

7 changes: 7 additions & 0 deletions src/icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { LabIcon } from '@jupyterlab/ui-components';
import deployApp from '../style/icons/deploy-app.svg';

export const deployAppIcon = new LabIcon({
name: 'jhub-apps:deploy-app',
svgstr: deployApp
});
79 changes: 58 additions & 21 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,68 @@ import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';
import { deployAppIcon } from './icons';
import { DocumentWidget } from '@jupyterlab/docregistry';

import { ISettingRegistry } from '@jupyterlab/settingregistry';
namespace CommandIDs {
/**
* Opens the URL to deploy the application with pre-populated fields
*/
export const deployApp = 'jhub-apps:deploy-app';
}

/**
* Initialization data for the jupyterlab-jhub-apps extension.
*/
const plugin: JupyterFrontEndPlugin<void> = {
id: 'jupyterlab-jhub-apps:plugin',
description: 'Customizations for jhub-apps.',
interface IDeployAppArgs {
/**
* The origin of the command e.g. main-menu, context-menu, etc.
*/
origin?: string;
}

const jhubAppsPlugin: JupyterFrontEndPlugin<void> = {
id: 'jupyterlab-jhub-apps:commands',
description: 'Adds additional commands used by jhub-apps.',
autoStart: true,
optional: [ISettingRegistry],
activate: (app: JupyterFrontEnd, settingRegistry: ISettingRegistry | null) => {
console.log('JupyterLab extension jupyterlab-jhub-apps is activated!');
activate: (app: JupyterFrontEnd) => {
const openURL = (url: string) => {
try {
window.open(url, '_blank', 'noopener,noreferrer');
} catch (error) {
console.warn(`Error opening ${url}: ${error}`);
}
};

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

if (settingRegistry) {
settingRegistry
.load(plugin.id)
.then(settings => {
console.log('jupyterlab-jhub-apps settings loaded:', settings.composite);
})
.catch(reason => {
console.error('Failed to load settings for jupyterlab-jhub-apps.', reason);
});
}
app.commands.addCommand(CommandIDs.deployApp, {
execute: () => {
const currentWidget = app.shell.currentWidget;
const currentNotebookPath =
currentWidget && currentWidget instanceof DocumentWidget
? currentWidget.context.path
: '';
let deployUrl;
if (currentNotebookPath !== '') {
deployUrl = `/services/japps/create-app?filepath=${encodeURIComponent(currentNotebookPath)}`;
} else {
deployUrl = '/services/japps/create-app';
}
openURL(deployUrl);
},
label: 'Deploy App',
icon: calculateIcon
});
}
};

export default plugin;
const plugins = [jhubAppsPlugin];

export default plugins;
4 changes: 4 additions & 0 deletions src/typings.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module '*.svg' {
const script: string;
export default script;
}
3 changes: 3 additions & 0 deletions style/icons/deploy-app.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
144 changes: 131 additions & 13 deletions ui-tests/tests/jupyterlab_jhub_apps.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,139 @@
import { expect, test } from '@jupyterlab/galata';

/**
* Don't load JupyterLab webpage before running the tests.
* This is required to ensure we capture all log messages.
*/
test.use({ autoGoto: false });
test('should have Deploy App entry in Services menu', async ({ page }) => {
await page.click('text=Services');

test('should emit an activation console message', async ({ page }) => {
const logs: string[] = [];
const deployAppEntry = page.locator('.lm-Menu-item:has-text("Deploy App")');
await expect(deployAppEntry).toBeVisible();

page.on('console', message => {
logs.push(message.text());
const servicesMenu = page.locator('.lm-Menu-content');
expect(await servicesMenu.screenshot()).toMatchSnapshot(
'services-menu-with-deploy-app.png'
);
});

test('should have Deploy App icon in notebook toolbar', async ({ page }) => {
await page.notebook.createNew();

await page.waitForSelector('.jp-NotebookPanel-toolbar');

const deployAppIcon = page.locator(
'.jp-Toolbar-item[data-jp-item-name="deploy-app"]'
);
await expect(deployAppIcon).toBeVisible();

// hack to hide kernel status indicator - otherwise toggle between idle and busy on startup
// and test will fail randomly
await page.evaluate(() => {
const kernelStatus = document.querySelector(
'.jp-NotebookPanel-toolbar .jp-Notebook-ExecutionIndicator'
) as HTMLElement;
if (kernelStatus) {
kernelStatus.style.display = 'none';
}
});

const notebookToolbar = page.locator('.jp-NotebookPanel-toolbar');
expect(await notebookToolbar.screenshot()).toMatchSnapshot(
'notebook-toolbar-before-click.png'
);
});

test('should show Deploy App option in context menu', async ({ page }) => {
await page.click('text=Python 3');
await page.waitForSelector('.jp-NotebookPanel');

await page.waitForSelector('.jp-DirListing-item[data-isdir="false"]');

const notebookItem = page
.locator('.jp-DirListing-item[data-isdir="false"]')
.first();
await notebookItem.click({ button: 'right' });

const deployAppOption = page.locator('.lm-Menu-item:has-text("Deploy App")');
await expect(deployAppOption).toBeVisible();

const contextMenu = page.locator('.lm-Menu-content');
expect(await contextMenu.screenshot()).toMatchSnapshot(
'notebook-context-menu-with-deploy-app.png'
);
});

test.describe('Deploy App with different notebook names to test URL encoding', () => {
const testCases = [
{ name: 'My Notebook.ipynb', expected: 'My%20Notebook.ipynb' },
{ name: 'Untitled.ipynb', expected: 'Untitled.ipynb' },
{
name: 'special!@#$%^&*().ipynb',
expected: 'special!%40%23%24%25%5E%26*().ipynb'
}
];

testCases.forEach(({ name, expected }) => {
test(`should generate correct encoding for "${name}"`, async ({
page,
context,
tmpPath
}) => {
await page.notebook.createNew(name);

const notebookItem = page
.locator('.jp-DirListing-item[data-isdir="false"]')
.first();
await notebookItem.click({ button: 'right' });
const deployAppOption = page.locator(
'.lm-Menu-item:has-text("Deploy App")'
);
await deployAppOption.click();

const newPage = await context.waitForEvent('page');
await newPage.waitForLoadState('load');

const fullUrl = newPage.url();
const filepathParam = fullUrl.split('filepath=')[1];
expect(filepathParam).toBe(tmpPath + '%2F' + expected);

await newPage.close();
});
});
});

test('check that the filepath parameter is not present in the URL when no notebook is open', async ({
page,
context
}) => {
const newPagePromise = context.waitForEvent('page');

await page.evaluate(() => {
window.jupyterapp.commands.execute('jhub-apps:deploy-app');
});

await page.goto();
const newPage = await newPagePromise;

expect(
logs.filter(s => s === 'JupyterLab extension jupyterlab-jhub-apps is activated!')
).toHaveLength(1);
await newPage.waitForLoadState('load');

const url = new URL(newPage.url());
expect(url.searchParams.has('filepath')).toBe(false);

await newPage.close();
});

test.describe('should register custom commands', () => {
test('jhub-apps:deploy-app command works', async ({ page }) => {
const deployAppMainMenu = await page.evaluate(async () => {
const registry = window.jupyterapp.commands;
const id = 'jhub-apps:deploy-app';
const args = { origin: 'main-menu' };

return {
id,
label: registry.label(id, args),
isEnabled: registry.isEnabled(id, args)
};
});

expect(deployAppMainMenu.label).toBe('Deploy App');

expect(deployAppMainMenu.isEnabled).toBe(true);
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 0e0faf6

Please sign in to comment.