diff --git a/schema/drives-file-browser.json b/schema/drives-file-browser.json new file mode 100644 index 0000000..aabaeea --- /dev/null +++ b/schema/drives-file-browser.json @@ -0,0 +1,102 @@ +{ + "title": "Jupyter Drives Settings", + "description": "jupyter-drives settings.", + "jupyter.lab.toolbars": { + "DriveBrowser": [ + { + "name": "new-launcher", + "command": "launcher:create", + "rank": 1 + }, + { + "name": "new-directory", + "command": "filebrowser:create-new-directory", + "rank": 10 + }, + { "name": "uploader", "rank": 20 }, + { + "name": "refresh", + "command": "filebrowser:refresh", + "rank": 30 + }, + { + "name": "toggle-file-filter", + "command": "filebrowser:toggle-file-filter", + "rank": 40 + }, + { + "name": "file-name-searcher", + "rank": 50 + } + ] + }, + "jupyter.lab.setting-icon": "@jupyter/drives:drive-browser", + "jupyter.lab.setting-icon-label": "Drive Browser", + "type": "object", + "jupyter.lab.transform": true, + "properties": { + "toolbar": { + "title": "Drive browser toolbar items", + "description": "Note: To disable a toolbar item,\ncopy it to User Preferences and add the\n\"disabled\" key.", + "items": { + "$ref": "#/definitions/toolbarItem" + }, + "type": "array", + "default": [] + } + }, + "additionalProperties": false, + "definitions": { + "toolbarItem": { + "properties": { + "name": { + "title": "Unique name", + "type": "string" + }, + "args": { + "title": "Command arguments", + "type": "object" + }, + "command": { + "title": "Command id", + "type": "string", + "default": "" + }, + "disabled": { + "title": "Whether the item is ignored or not", + "type": "boolean", + "default": false + }, + "icon": { + "title": "Item icon id", + "description": "If defined, it will override the command icon", + "type": "string" + }, + "label": { + "title": "Item label", + "description": "If defined, it will override the command label", + "type": "string" + }, + "caption": { + "title": "Item caption", + "description": "If defined, it will override the command caption", + "type": "string" + }, + "type": { + "title": "Item type", + "type": "string", + "enum": ["command", "spacer"] + }, + "rank": { + "title": "Item rank", + "type": "number", + "minimum": 0, + "default": 50 + } + }, + "required": ["name"], + "additionalProperties": false, + "type": "object" + } + } +} diff --git a/schema/widget.json b/schema/widget.json deleted file mode 100644 index c450b97..0000000 --- a/schema/widget.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "jupyter.lab.toolbars": { - "FileBrowser": [ - { - "name": "new-directory", - "command": "filebrowser:create-new-directory", - "rank": 10 - }, - { "name": "uploader", "rank": 20 }, - { "name": "refresh", "command": "filebrowser:refresh", "rank": 30 }, - { - "name": "drive", - "command": "drives:open-drives-dialog", - "rank": 35 - }, - { "name": "fileNameSearcher", "rank": 40 } - ] - }, - "title": "'@jupyter/drives", - "description": "jupyter-drives settings.", - "type": "object", - "properties": {}, - "additionalProperties": false -} diff --git a/src/contents.ts b/src/contents.ts new file mode 100644 index 0000000..1db63d1 --- /dev/null +++ b/src/contents.ts @@ -0,0 +1,583 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { Signal, ISignal } from '@lumino/signaling'; +import { Contents, ServerConnection } from '@jupyterlab/services'; + +const data: Contents.IModel = { + name: '', + path: '', + last_modified: '', + created: '', + content: [], + format: null, + mimetype: '', + size: 0, + writable: true, + type: '' +}; + +export class Drive implements Contents.IDrive { + /** + * Construct a new drive object. + * + * @param options - The options used to initialize the object. + */ + constructor(options: Drive.IOptions = {}) { + this._serverSettings = ServerConnection.makeSettings(); + this._name = options.name ?? ''; + //this._apiEndpoint = options.apiEndpoint ?? SERVICE_DRIVE_URL; + } + /** + * The Drive base URL + */ + get baseUrl(): string { + return this._baseUrl; + } + + /** + * The Drive base URL is set by the settingsRegistry change hook + */ + set baseUrl(url: string) { + this._baseUrl = url; + } + /** + * The Drive name getter + */ + get name(): string { + return this._name; + } + + /** + * The Drive name setter */ + set name(name: string) { + this._name = name; + } + + /** + * The Drive provider getter + */ + get provider(): string { + return this._provider; + } + + /** + * The Drive provider setter */ + set provider(name: string) { + this._provider = name; + } + + /** + * The Drive region getter + */ + get region(): string { + return this._region; + } + + /** + * The Drive region setter */ + set region(region: string) { + this._region = region; + } + + /** + * The Drive creationDate getter + */ + get creationDate(): string { + return this._creationDate; + } + + /** + * The Drive region setter */ + set creationDate(date: string) { + this._creationDate = date; + } + + /** + * Settings for the notebook server. + */ + get serverSettings(): ServerConnection.ISettings { + return this._serverSettings; + } + + /** + * A signal emitted when a file operation takes place. + */ + get fileChanged(): ISignal { + return this._fileChanged; + } + + /** + * Test whether the manager has been disposed. + */ + get isDisposed(): boolean { + return this._isDisposed; + } + + /** + * A signal emitted when the drive is disposed. + */ + get disposed(): ISignal { + return this._disposed; + } + + /** + * Dispose of the resources held by the manager. + */ + dispose(): void { + if (this.isDisposed) { + return; + } + this._isDisposed = true; + this._disposed.emit(); + Signal.clearData(this); + } + + /** + * Get an encoded download url given a file path. + * + * @param path - An absolute POSIX file path on the server. + * + * #### Notes + * It is expected that the path contains no relative paths, + * use [[ContentsManager.getAbsolutePath]] to get an absolute + * path if necessary. + */ + getDownloadUrl(path: string): Promise { + // Parse the path into user/repo/path + return Promise.reject('Empty getDownloadUrl method'); + } + + /** + * Get a file or directory. + * + * @param localPath: The path to the file. + * + * @param options: The options used to fetch the file. + * + * @returns A promise which resolves with the file content. + * + * Uses the [Jupyter Notebook API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents) and validates the response model. + */ + async get( + localPath: string, + options?: Contents.IFetchOptions + ): Promise { + let relativePath = ''; + if (localPath !== '') { + if (localPath.includes(this.name)) { + relativePath = localPath.split(this.name + '/')[1]; + } else { + relativePath = localPath; + } + } + console.log('GET: ', relativePath); + + Contents.validateContentsModel(data); + return data; + } + + /** + * Create a new untitled file or directory in the specified directory path. + * + * @param options: The options used to create the file. + * + * @returns A promise which resolves with the created file content when the + * file is created. + */ + + async newUntitled( + options: Contents.ICreateOptions = {} + ): Promise { + /*let body = '{}'; + if (options) { + if (options.ext) { + options.ext = Private.normalizeExtension(options.ext); + } + body = JSON.stringify(options); + } + + const settings = this.serverSettings; + const url = this._getUrl(options.path ?? ''); + const init = { + method: 'POST', + body + }; + const response = await ServerConnection.makeRequest(url, init, settings); + if (response.status !== 201) { + const err = await ServerConnection.ResponseError.create(response); + throw err; + } + const data = await response.json();*/ + + Contents.validateContentsModel(data); + this._fileChanged.emit({ + type: 'new', + oldValue: null, + newValue: data + }); + + return data; + } + + incrementUntitledName( + contents: Contents.IModel, + options: Contents.ICreateOptions + ): string { + const content: Array = contents.content; + let name: string = ''; + let countText = 0; + let countDir = 0; + let countNotebook = 0; + + content.forEach(item => { + if (options.ext !== undefined) { + if (item.name.includes('untitled') && item.name.includes('.txt')) { + countText = countText + 1; + } else if ( + item.name.includes('Untitled') && + item.name.includes('.ipynb') + ) { + countNotebook = countNotebook + 1; + } + } else if (item.name.includes('Untitled Folder')) { + countDir = countDir + 1; + } + }); + + if (options.ext === 'txt') { + if (countText === 0) { + name = 'untitled' + '.' + options.ext; + } else { + name = 'untitled' + countText + '.' + options.ext; + } + } + if (options.ext === 'ipynb') { + if (countNotebook === 0) { + name = 'Untitled' + '.' + options.ext; + } else { + name = 'Untitled' + countNotebook + '.' + options.ext; + } + } else if (options.type === 'directory') { + if (countDir === 0) { + name = 'Untitled Folder'; + } else { + name = 'Untitled Folder ' + countDir; + } + } + return name; + } + + /** + * Delete a file. + * + * @param path - The path to the file. + * + * @returns A promise which resolves when the file is deleted. + */ + /*delete(path: string): Promise { + return Promise.reject('Repository is read only'); + }*/ + + async delete(localPath: string): Promise { + /*const url = this._getUrl(localPath); + const settings = this.serverSettings; + const init = { method: 'DELETE' }; + const response = await ServerConnection.makeRequest(url, init, settings); + // TODO: update IPEP27 to specify errors more precisely, so + // that error types can be detected here with certainty. + if (response.status !== 204) { + const err = await ServerConnection.ResponseError.create(response); + throw err; + }*/ + + this._fileChanged.emit({ + type: 'delete', + oldValue: { path: localPath }, + newValue: null + }); + } + + /** + * Rename a file or directory. + * + * @param oldLocalPath - The original file path. + * + * @param newLocalPath - The new file path. + * + * @returns A promise which resolves with the new file contents model when + * the file is renamed. + * + * #### Notes + * Uses the [Jupyter Notebook API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents) and validates the response model. + */ + async rename( + oldLocalPath: string, + newLocalPath: string, + options: Contents.ICreateOptions = {} + ): Promise { + /*const settings = this.serverSettings; + const url = this._getUrl(oldLocalPath); + const init = { + method: 'PATCH', + body: JSON.stringify({ path: newLocalPath }) + }; + const response = await ServerConnection.makeRequest(url, init, settings); + if (response.status !== 200) { + const err = await ServerConnection.ResponseError.create(response); + throw err; + } + const data = await response.json();*/ + + this._fileChanged.emit({ + type: 'rename', + oldValue: { path: oldLocalPath }, + newValue: { path: newLocalPath } + }); + Contents.validateContentsModel(data); + return data; + } + /** + * Save a file. + * + * @param localPath - The desired file path. + * + * @param options - Optional overrides to the model. + * + * @returns A promise which resolves with the file content model when the + * file is saved. + * + * #### Notes + * Ensure that `model.content` is populated for the file. + * + * Uses the [Jupyter Notebook API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents) and validates the response model. + */ + async save( + localPath: string, + options: Partial = {} + ): Promise { + /*const settings = this.serverSettings; + const url = this._getUrl(localPath); + const init = { + method: 'PUT', + body: JSON.stringify(options) + }; + const response = await ServerConnection.makeRequest(url, init, settings); + // will return 200 for an existing file and 201 for a new file + if (response.status !== 200 && response.status !== 201) { + const err = await ServerConnection.ResponseError.create(response); + throw err; + } + const data = await response.json();*/ + + Contents.validateContentsModel(data); + this._fileChanged.emit({ + type: 'save', + oldValue: null, + newValue: data + }); + return data; + } + + /** + * Copy a file into a given directory. + * + * @param path - The original file path. + * + * @param toDir - The destination directory path. + * + * @returns A promise which resolves with the new contents model when the + * file is copied. + */ + + incrementCopyName(contents: Contents.IModel, copiedItemPath: string): string { + const content: Array = contents.content; + let name: string = ''; + let countText = 0; + let countDir = 0; + let countNotebook = 0; + let ext = undefined; + const list1 = copiedItemPath.split('/'); + const copiedItemName = list1[list1.length - 1]; + + const list2 = copiedItemName.split('.'); + let rootName = list2[0]; + + content.forEach(item => { + if (item.name.includes(rootName) && item.name.includes('.txt')) { + ext = '.txt'; + if (rootName.includes('-Copy')) { + const list3 = rootName.split('-Copy'); + countText = parseInt(list3[1]) + 1; + rootName = list3[0]; + } else { + countText = countText + 1; + } + } + if (item.name.includes(rootName) && item.name.includes('.ipynb')) { + ext = '.ipynb'; + if (rootName.includes('-Copy')) { + const list3 = rootName.split('-Copy'); + countNotebook = parseInt(list3[1]) + 1; + rootName = list3[0]; + } else { + countNotebook = countNotebook + 1; + } + } else if (item.name.includes(rootName)) { + if (rootName.includes('-Copy')) { + const list3 = rootName.split('-Copy'); + countDir = parseInt(list3[1]) + 1; + rootName = list3[0]; + } else { + countDir = countDir + 1; + } + } + }); + + if (ext === '.txt') { + name = rootName + '-Copy' + countText + ext; + } + if (ext === 'ipynb') { + name = rootName + '-Copy' + countText + ext; + } else if (ext === undefined) { + name = rootName + '-Copy' + countDir; + } + + return name; + } + async copy( + fromFile: string, + toDir: string, + options: Contents.ICreateOptions = {} + ): Promise { + /*const settings = this.serverSettings; + const url = this._getUrl(toDir); + const init = { + method: 'POST', + body: JSON.stringify({ copy_from: fromFile }) + }; + const response = await ServerConnection.makeRequest(url, init, settings); + if (response.status !== 201) { + const err = await ServerConnection.ResponseError.create(response); + throw err; + } + const data = await response.json();*/ + + this._fileChanged.emit({ + type: 'new', + oldValue: null, + newValue: data + }); + Contents.validateContentsModel(data); + return data; + } + + /** + * Create a checkpoint for a file. + * + * @param path - The path of the file. + * + * @returns A promise which resolves with the new checkpoint model when the + * checkpoint is created. + */ + createCheckpoint(path: string): Promise { + return Promise.reject('Repository is read only'); + } + + /** + * List available checkpoints for a file. + * + * @param path - The path of the file. + * + * @returns A promise which resolves with a list of checkpoint models for + * the file. + */ + listCheckpoints(path: string): Promise { + return Promise.resolve([]); + } + + /** + * Restore a file to a known checkpoint state. + * + * @param path - The path of the file. + * + * @param checkpointID - The id of the checkpoint to restore. + * + * @returns A promise which resolves when the checkpoint is restored. + */ + restoreCheckpoint(path: string, checkpointID: string): Promise { + return Promise.reject('Repository is read only'); + } + + /** + * Delete a checkpoint for a file. + * + * @param path - The path of the file. + * + * @param checkpointID - The id of the checkpoint to delete. + * + * @returns A promise which resolves when the checkpoint is deleted. + */ + deleteCheckpoint(path: string, checkpointID: string): Promise { + return Promise.reject('Read only'); + } + + /** + * Get a REST url for a file given a path. + */ + /*private _getUrl(...args: string[]): string { + const parts = args.map(path => URLExt.encodeParts(path)); + const baseUrl = this.serverSettings.baseUrl; + return URLExt.join(baseUrl, this._apiEndpoint, ...parts); + }*/ + + // private _apiEndpoint: string; + private _serverSettings: ServerConnection.ISettings; + private _name: string = ''; + private _provider: string = ''; + private _baseUrl: string = ''; + private _region: string = ''; + private _creationDate: string = ''; + private _fileChanged = new Signal(this); + private _isDisposed: boolean = false; + private _disposed = new Signal(this); +} + +export namespace Drive { + /** + * The options used to initialize a `Drive`. + */ + export interface IOptions { + /** + * The name for the `Drive`, which is used in file + * paths to disambiguate it from other drives. + */ + name?: string; + + /** + * The server settings for the server. + */ + serverSettings?: ServerConnection.ISettings; + + /** + * A REST endpoint for drive requests. + * If not given, defaults to the Jupyter + * REST API given by [Jupyter Notebook API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents). + */ + apiEndpoint?: string; + } +} + +/*namespace Private { + /** + * Normalize a file extension to be of the type `'.foo'`. + * + * Adds a leading dot if not present and converts to lower case. + */ +/*export function normalizeExtension(extension: string): string { + if (extension.length > 0 && extension.indexOf('.') !== 0) { + extension = `.${extension}`; + } + return extension; + } +}*/ diff --git a/src/icons.ts b/src/icons.ts index 3f4bb13..a54d0ab 100644 --- a/src/icons.ts +++ b/src/icons.ts @@ -1,6 +1,13 @@ import { LabIcon } from '@jupyterlab/ui-components'; import driveSvgstr from '../style/drive.svg'; +import driveBrowserSvg from '../style/driveIconFileBrowser.svg'; + export const DriveIcon = new LabIcon({ name: '@jupyter/drives:drive', svgstr: driveSvgstr }); + +export const driveBrowserIcon = new LabIcon({ + name: '@jupyter/drives:drive-browser', + svgstr: driveBrowserSvg +}); diff --git a/src/index.ts b/src/index.ts index 7402dcd..c815a0f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,30 +1,51 @@ import { + ILabShell, + ILayoutRestorer, + IRouter, JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; - -import { IFileBrowserFactory } from '@jupyterlab/filebrowser'; +import { + IFileBrowserFactory, + FileBrowser, + Uploader +} from '@jupyterlab/filebrowser'; import { ITranslator } from '@jupyterlab/translation'; import { addJupyterLabThemeChangeListener } from '@jupyter/web-components'; -import { Dialog, showDialog } from '@jupyterlab/apputils'; +import { + createToolbarFactory, + IToolbarWidgetRegistry, + setToolbar, + Dialog, + showDialog +} from '@jupyterlab/apputils'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; +import { FilenameSearcher, IScore } from '@jupyterlab/ui-components'; +import { CommandRegistry } from '@lumino/commands'; +import { Panel } from '@lumino/widgets'; + import { DriveListModel, DriveListView, IDrive } from './drivelistmanager'; -import { DriveIcon } from './icons'; +import { DriveIcon, driveBrowserIcon } from './icons'; +import { Drive } from './contents'; +/** + * The command IDs used by the driveBrowser plugin. + */ namespace CommandIDs { export const openDrivesDialog = 'drives:open-drives-dialog'; + export const openPath = 'drives:open-path'; + export const toggleBrowser = 'drives:toggle-main'; } /** - * Initialization data for the @jupyter/drives extension. + * The file browser factory ID. */ -const plugin: JupyterFrontEndPlugin = { - id: '@jupyter/drives:plugin', - description: 'A Jupyter extension to support drives in the backend.', - autoStart: true, - activate: (app: JupyterFrontEnd) => { - console.log('JupyterLab extension @jupyter/drives is activated!'); - } -}; +const FILE_BROWSER_FACTORY = 'DriveBrowser'; + +/** + * The class name added to the drive filebrowser filterbox node. + */ +const FILTERBOX_CLASS = 'jp-DriveBrowser-filterBox'; const openDriveDialogPlugin: JupyterFrontEndPlugin = { id: '@jupyter/drives:widget', @@ -129,5 +150,175 @@ const openDriveDialogPlugin: JupyterFrontEndPlugin = { }); } }; -const plugins: JupyterFrontEndPlugin[] = [plugin, openDriveDialogPlugin]; + +/** + * The drive file browser factory provider. + */ +const driveFileBrowser: JupyterFrontEndPlugin = { + id: '@jupyter/drives:drives-file-browser', + description: 'The drive file browser factory provider.', + autoStart: true, + requires: [ + IFileBrowserFactory, + IToolbarWidgetRegistry, + ISettingRegistry, + ITranslator + ], + optional: [ + IRouter, + JupyterFrontEnd.ITreeResolver, + ILabShell, + ILayoutRestorer + ], + activate: async ( + app: JupyterFrontEnd, + fileBrowserFactory: IFileBrowserFactory, + toolbarRegistry: IToolbarWidgetRegistry, + settingsRegistry: ISettingRegistry, + translator: ITranslator, + router: IRouter | null, + tree: JupyterFrontEnd.ITreeResolver | null, + labShell: ILabShell | null, + restorer: ILayoutRestorer | null + ): Promise => { + console.log( + 'JupyterLab extension @jupyter/drives:drives-file-browser is activated!' + ); + const { commands } = app; + + // create drive for drive file browser + const drive = new Drive({ + name: 'jupyter-drives-buckets' + }); + + app.serviceManager.contents.addDrive(drive); + + // Manually restore and load the drive file browser. + const driveBrowser = fileBrowserFactory.createFileBrowser('drivebrowser', { + auto: false, + restore: false, + driveName: drive.name + }); + + // Set attributes when adding the browser to the UI + driveBrowser.node.setAttribute('role', 'region'); + driveBrowser.node.setAttribute('aria-label', 'Drive Browser Section'); + + void Private.restoreBrowser(driveBrowser, commands, router, tree, labShell); + + toolbarRegistry.addFactory( + FILE_BROWSER_FACTORY, + 'uploader', + (fileBrowser: FileBrowser) => + new Uploader({ model: fileBrowser.model, translator }) + ); + + toolbarRegistry.addFactory( + FILE_BROWSER_FACTORY, + 'file-name-searcher', + (fileBrowser: FileBrowser) => { + const searcher = FilenameSearcher({ + updateFilter: ( + filterFn: (item: string) => Partial | null, + query?: string + ) => { + fileBrowser.model.setFilter(value => { + return filterFn(value.name.toLowerCase()); + }); + }, + useFuzzyFilter: true, + placeholder: 'Filter files by names', + forceRefresh: true + }); + searcher.addClass(FILTERBOX_CLASS); + return searcher; + } + ); + + // connect the filebrowser toolbar to the settings registry for the plugin + setToolbar( + driveBrowser, + createToolbarFactory( + toolbarRegistry, + settingsRegistry, + FILE_BROWSER_FACTORY, + driveFileBrowser.id, + translator + ) + ); + + // instate Drive Browser Panel + const drivePanel = new Panel(); + drivePanel.title.icon = driveBrowserIcon; + drivePanel.title.iconClass = 'jp-sideBar-tabIcon'; + drivePanel.title.caption = 'Drive File Browser'; + drivePanel.id = 'Drive-Browser-Panel'; + + app.shell.add(drivePanel, 'left', { rank: 102, type: 'File Browser' }); + drivePanel.addWidget(driveBrowser); + if (restorer) { + restorer.add(drivePanel, 'drive-sidepanel'); + } + } +}; + +const plugins: JupyterFrontEndPlugin[] = [ + driveFileBrowser, + openDriveDialogPlugin +]; export default plugins; + +namespace Private { + /** + * Restores file browser state and overrides state if tree resolver resolves. + */ + export async function restoreBrowser( + browser: FileBrowser, + commands: CommandRegistry, + router: IRouter | null, + tree: JupyterFrontEnd.ITreeResolver | null, + labShell: ILabShell | null + ): Promise { + const restoring = 'jp-mod-restoring'; + + browser.addClass(restoring); + + if (!router) { + await browser.model.restore(browser.id); + await browser.model.refresh(); + browser.removeClass(restoring); + return; + } + + const listener = async () => { + router.routed.disconnect(listener); + + const paths = await tree?.paths; + if (paths?.file || paths?.browser) { + // Restore the model without populating it. + await browser.model.restore(browser.id, false); + if (paths.file) { + await commands.execute(CommandIDs.openPath, { + path: paths.file, + dontShowBrowser: true + }); + } + if (paths.browser) { + await commands.execute(CommandIDs.openPath, { + path: paths.browser, + dontShowBrowser: true + }); + } + } else { + await browser.model.restore(browser.id); + await browser.model.refresh(); + } + browser.removeClass(restoring); + + if (labShell?.isEmpty('main')) { + void commands.execute('launcher:create'); + } + }; + router.routed.connect(listener); + } +} diff --git a/style/driveIconFileBrowser.svg b/style/driveIconFileBrowser.svg new file mode 100644 index 0000000..fca590c --- /dev/null +++ b/style/driveIconFileBrowser.svg @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/ui-tests/tests/jupyter_drives.spec.ts b/ui-tests/tests/jupyter_drives.spec.ts index dd84496..9dee802 100644 --- a/ui-tests/tests/jupyter_drives.spec.ts +++ b/ui-tests/tests/jupyter_drives.spec.ts @@ -16,6 +16,10 @@ test('should emit an activation console message', async ({ page }) => { await page.goto(); expect( - logs.filter(s => s === 'JupyterLab extension @jupyter/drives is activated!') + logs.filter( + s => + s === + 'JupyterLab extension @jupyter/drives:drives-file-browser is activated!' + ) ).toHaveLength(1); });