diff --git a/packages/notebook-extension/schema/full-width-notebook.json b/packages/notebook-extension/schema/full-width-notebook.json new file mode 100644 index 000000000..03189ce2f --- /dev/null +++ b/packages/notebook-extension/schema/full-width-notebook.json @@ -0,0 +1,27 @@ +{ + "title": "Jupyter Notebook Full Width Notebook", + "description": "Jupyter Notebook Notebook With settings", + "jupyter.lab.menus": { + "main": [ + { + "id": "jp-mainmenu-view", + "items": [ + { + "command": "notebook:toggle-full-width", + "rank": 4 + } + ] + } + ] + }, + "properties": { + "fullWidthNotebook": { + "type": "boolean", + "title": "Full Width Notebook", + "description": "Whether to the notebook should take up the full width of the application", + "default": false + } + }, + "additionalProperties": false, + "type": "object" +} diff --git a/packages/notebook-extension/src/index.ts b/packages/notebook-extension/src/index.ts index ca1f2e89e..67f6af72b 100644 --- a/packages/notebook-extension/src/index.ts +++ b/packages/notebook-extension/src/index.ts @@ -64,6 +64,11 @@ const KERNEL_STATUS_FADE_OUT_CLASS = 'jp-NotebookKernelStatus-fade'; */ const SCROLLED_OUTPUTS_CLASS = 'jp-mod-outputsScrolled'; +/** + * The class for the full width notebook + */ +const FULL_WIDTH_NOTEBOOK_CLASS = 'jp-mod-fullwidth'; + /** * The command IDs used by the notebook plugins. */ @@ -72,6 +77,11 @@ namespace CommandIDs { * A command to open right sidebar for Editing Notebook Metadata */ export const openEditNotebookMetadata = 'notebook:edit-metadata'; + + /** + * A command to toggle full width of the notebook + */ + export const toggleFullWidth = 'notebook:toggle-full-width'; } /** @@ -202,6 +212,83 @@ const openTreeTab: JupyterFrontEndPlugin = { }, }; +/** + * A plugin to set the notebook to full width. + */ +const fullWidthNotebook: JupyterFrontEndPlugin = { + id: '@jupyter-notebook/notebook-extension:full-width-notebook', + description: 'A plugin to set the notebook to full width.', + autoStart: true, + requires: [INotebookTracker], + optional: [ICommandPalette, ISettingRegistry, ITranslator], + activate: ( + app: JupyterFrontEnd, + tracker: INotebookTracker, + palette: ICommandPalette | null, + settingRegistry: ISettingRegistry | null, + translator: ITranslator | null + ) => { + const trans = (translator ?? nullTranslator).load('notebook'); + + let fullWidth = false; + + const toggleFullWidth = () => { + const current = tracker.currentWidget; + fullWidth = !fullWidth; + if (!current) { + return; + } + const content = current; + content.toggleClass(FULL_WIDTH_NOTEBOOK_CLASS, fullWidth); + }; + + let notebookSettings: ISettingRegistry.ISettings; + + if (settingRegistry) { + const loadSettings = settingRegistry.load(fullWidthNotebook.id); + + const updateSettings = (settings: ISettingRegistry.ISettings): void => { + const newFullWidth = settings.get('fullWidthNotebook') + .composite as boolean; + if (newFullWidth !== fullWidth) { + toggleFullWidth(); + } + }; + + Promise.all([loadSettings, app.restored]) + .then(([settings]) => { + notebookSettings = settings; + updateSettings(settings); + settings.changed.connect((settings) => { + updateSettings(settings); + }); + }) + .catch((reason: Error) => { + console.error(reason.message); + }); + } + + app.commands.addCommand(CommandIDs.toggleFullWidth, { + label: trans.__('Enable Full Width Notebook'), + execute: () => { + toggleFullWidth(); + if (notebookSettings) { + notebookSettings.set('fullWidthNotebook', fullWidth); + } + }, + isEnabled: () => tracker.currentWidget !== null, + isToggled: () => fullWidth, + }); + + if (palette) { + palette.addItem({ + command: CommandIDs.toggleFullWidth, + category: 'Notebook Operations', + }); + } + }, +}; + /** * The kernel logo plugin. */ @@ -597,6 +684,7 @@ const plugins: JupyterFrontEndPlugin[] = [ closeTab, openTreeTab, editNotebookMetadata, + fullWidthNotebook, kernelLogo, kernelStatus, notebookToolsWidget, diff --git a/packages/notebook-extension/style/base.css b/packages/notebook-extension/style/base.css index 3a6de880d..e34a06bee 100644 --- a/packages/notebook-extension/style/base.css +++ b/packages/notebook-extension/style/base.css @@ -16,6 +16,16 @@ - compact view on mobile */ +/* Make the notebook take up the full width of the page when jp-mod-fullwidth is set */ + +body[data-notebook='notebooks'] + .jp-NotebookPanel.jp-mod-fullwidth + .jp-WindowedPanel-outer { + padding-left: unset; + padding-right: unset !important; + width: unset; +} + /* Keep the notebook centered on the page */ body[data-notebook='notebooks'] .jp-NotebookPanel-toolbar { diff --git a/ui-tests/test/general.spec.ts b/ui-tests/test/general.spec.ts index eea5f18b7..aebfd37c7 100644 --- a/ui-tests/test/general.spec.ts +++ b/ui-tests/test/general.spec.ts @@ -7,7 +7,7 @@ import { expect } from '@jupyterlab/galata'; import { test } from './fixtures'; -import { hideAddCellButton, waitForKernelReady } from './utils'; +import { waitForNotebook } from './utils'; test.describe('General', () => { test('The notebook should render', async ({ page, tmpPath, browserName }) => { @@ -18,23 +18,6 @@ test.describe('General', () => { ); await page.goto(`notebooks/${tmpPath}/${notebook}`); - // wait for the kernel status animations to be finished - await waitForKernelReady(page); - await page.waitForSelector( - ".jp-Notebook-ExecutionIndicator[data-status='idle']" - ); - - const checkpointLocator = '.jp-NotebookCheckpoint'; - // wait for the checkpoint indicator to be displayed - await page.waitForSelector(checkpointLocator); - - // remove the amount of seconds manually since it might display strings such as "3 seconds ago" - await page - .locator(checkpointLocator) - .evaluate( - (element) => (element.innerHTML = 'Last Checkpoint: 3 seconds ago') - ); - // check the notebook footer shows up on hover const notebookFooter = '.jp-Notebook-footer'; await page.hover(notebookFooter); @@ -46,11 +29,8 @@ test.describe('General', () => { // click to make the blue border around the cell disappear await page.click('.jp-WindowedPanel-outer'); - // special case for firefox headless issue - // see https://github.com/jupyter/notebook/pull/6872#issuecomment-1549594166 for more details - if (browserName === 'firefox') { - await hideAddCellButton(page); - } + // wait for the notebook to be ready + await waitForNotebook(page, browserName); expect(await page.screenshot()).toMatchSnapshot('notebook.png'); }); diff --git a/ui-tests/test/menus.spec.ts-snapshots/opened-menu-view-chromium-linux.png b/ui-tests/test/menus.spec.ts-snapshots/opened-menu-view-chromium-linux.png index 7eac034b7..dd2a993c7 100644 Binary files a/ui-tests/test/menus.spec.ts-snapshots/opened-menu-view-chromium-linux.png and b/ui-tests/test/menus.spec.ts-snapshots/opened-menu-view-chromium-linux.png differ diff --git a/ui-tests/test/menus.spec.ts-snapshots/opened-menu-view-firefox-linux.png b/ui-tests/test/menus.spec.ts-snapshots/opened-menu-view-firefox-linux.png index 4e43e6c13..8c2b352ce 100644 Binary files a/ui-tests/test/menus.spec.ts-snapshots/opened-menu-view-firefox-linux.png and b/ui-tests/test/menus.spec.ts-snapshots/opened-menu-view-firefox-linux.png differ diff --git a/ui-tests/test/notebook.spec.ts b/ui-tests/test/notebook.spec.ts index f8ab023e5..4a7c2bcf9 100644 --- a/ui-tests/test/notebook.spec.ts +++ b/ui-tests/test/notebook.spec.ts @@ -7,7 +7,7 @@ import { expect } from '@jupyterlab/galata'; import { test } from './fixtures'; -import { runAndAdvance, waitForKernelReady } from './utils'; +import { waitForNotebook, runAndAdvance, waitForKernelReady } from './utils'; const NOTEBOOK = 'example.ipynb'; @@ -175,4 +175,35 @@ test.describe('Notebook', () => { expect(page.isClosed()); }); + + test('Toggle the full width of the notebook', async ({ + page, + browserName, + tmpPath, + }) => { + const notebook = 'simple.ipynb'; + await page.contents.uploadFile( + path.resolve(__dirname, `./notebooks/${notebook}`), + `${tmpPath}/${notebook}` + ); + await page.goto(`notebooks/${tmpPath}/${notebook}`); + + const menuPath = 'View>Enable Full Width Notebook'; + await page.menu.clickMenuItem(menuPath); + + const notebookPanel = page.locator('.jp-NotebookPanel').first(); + await expect(notebookPanel).toHaveClass(/jp-mod-fullwidth/); + + // click to make the blue border around the cell disappear + await page.click('.jp-WindowedPanel-outer'); + + // wait for the notebook to be ready + await waitForNotebook(page, browserName); + + expect(await page.screenshot()).toMatchSnapshot('notebook-full-width.png'); + + // undo the full width + await page.menu.clickMenuItem(menuPath); + await expect(notebookPanel).not.toHaveClass(/jp-mod-fullwidth/); + }); }); diff --git a/ui-tests/test/notebook.spec.ts-snapshots/notebook-full-width-chromium-linux.png b/ui-tests/test/notebook.spec.ts-snapshots/notebook-full-width-chromium-linux.png new file mode 100644 index 000000000..b4504f210 Binary files /dev/null and b/ui-tests/test/notebook.spec.ts-snapshots/notebook-full-width-chromium-linux.png differ diff --git a/ui-tests/test/notebook.spec.ts-snapshots/notebook-full-width-firefox-linux.png b/ui-tests/test/notebook.spec.ts-snapshots/notebook-full-width-firefox-linux.png new file mode 100644 index 000000000..aa602636a Binary files /dev/null and b/ui-tests/test/notebook.spec.ts-snapshots/notebook-full-width-firefox-linux.png differ diff --git a/ui-tests/test/settings.spec.ts-snapshots/top-visible-chromium-linux.png b/ui-tests/test/settings.spec.ts-snapshots/top-visible-chromium-linux.png index c653f8155..b9706f2e7 100644 Binary files a/ui-tests/test/settings.spec.ts-snapshots/top-visible-chromium-linux.png and b/ui-tests/test/settings.spec.ts-snapshots/top-visible-chromium-linux.png differ diff --git a/ui-tests/test/utils.ts b/ui-tests/test/utils.ts index 96747adcb..c8279134d 100644 --- a/ui-tests/test/utils.ts +++ b/ui-tests/test/utils.ts @@ -30,7 +30,9 @@ export async function waitForKernelReady(page: Page): Promise { }, true); return finished; }); - if (page.viewportSize()?.width > 600) { + const viewport = page.viewportSize(); + const width = viewport?.width; + if (width && width > 600) { await page.waitForSelector('.jp-DebuggerBugButton[aria-disabled="false"]'); } } @@ -44,3 +46,34 @@ export async function hideAddCellButton(page: Page): Promise { .locator('.jp-Notebook-footer') .evaluate((element) => (element.style.display = 'none')); } + +/** + * Wait for the notebook to be ready + */ +export async function waitForNotebook( + page: Page, + browserName = '' +): Promise { + // wait for the kernel status animations to be finished + await waitForKernelReady(page); + await page.waitForSelector( + ".jp-Notebook-ExecutionIndicator[data-status='idle']" + ); + + const checkpointLocator = '.jp-NotebookCheckpoint'; + // wait for the checkpoint indicator to be displayed + await page.waitForSelector(checkpointLocator); + + // remove the amount of seconds manually since it might display strings such as "3 seconds ago" + await page + .locator(checkpointLocator) + .evaluate( + (element) => (element.innerHTML = 'Last Checkpoint: 3 seconds ago') + ); + + // special case for firefox headless issue + // see https://github.com/jupyter/notebook/pull/6872#issuecomment-1549594166 for more details + if (browserName === 'firefox') { + await hideAddCellButton(page); + } +}