@jupyterlab/filebrowser
Version:
JupyterLab - FileBrowser Widget
667 lines (585 loc) • 17.2 kB
text/typescript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { showErrorMessage } from '@jupyterlab/apputils';
import { PathExt } from '@jupyterlab/coreutils';
import { IDocumentManager } from '@jupyterlab/docmanager';
import { Contents, ServerConnection } from '@jupyterlab/services';
import { IStateDB } from '@jupyterlab/statedb';
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
import {
FilenameSearcher,
IScore,
SidePanel,
Toolbar
} from '@jupyterlab/ui-components';
import { ISignal, Signal } from '@lumino/signaling';
import { Panel } from '@lumino/widgets';
import { createRef } from 'react';
import { BreadCrumbs } from './crumbs';
import { DirListing } from './listing';
import { FilterFileBrowserModel } from './model';
/**
* The class name added to file browsers.
*/
const FILE_BROWSER_CLASS = 'jp-FileBrowser';
/**
* The class name added to file browser panel (gather filter, breadcrumbs and listing).
*/
const FILE_BROWSER_PANEL_CLASS = 'jp-FileBrowser-Panel';
/**
* The class name added to the filebrowser crumbs node.
*/
const CRUMBS_CLASS = 'jp-FileBrowser-crumbs';
/**
* The class name added to the filebrowser toolbar node.
*/
const TOOLBAR_CLASS = 'jp-FileBrowser-toolbar';
/**
* The class name added to the filebrowser filter toolbar node.
*/
const FILTER_TOOLBAR_CLASS = 'jp-FileBrowser-filterToolbar';
/**
* The class name added to the filebrowser listing node.
*/
const LISTING_CLASS = 'jp-FileBrowser-listing';
/**
* The class name added to the filebrowser filterbox node.
*/
const FILTERBOX_CLASS = 'jp-FileBrowser-filterBox';
/**
* A widget which hosts a file browser.
*
* The widget uses the Jupyter Contents API to retrieve contents,
* and presents itself as a flat list of files and directories with
* breadcrumbs.
*/
export class FileBrowser extends SidePanel {
/**
* Construct a new file browser.
*
* @param options - The file browser options.
*/
constructor(options: FileBrowser.IOptions) {
super({ content: new Panel(), translator: options.translator });
this.addClass(FILE_BROWSER_CLASS);
this.toolbar.addClass(TOOLBAR_CLASS);
this.id = options.id;
const translator = (this.translator = options.translator ?? nullTranslator);
const model = (this.model = options.model);
const renderer = options.renderer;
model.connectionFailure.connect(this._onConnectionFailure, this);
this._manager = model.manager;
this.toolbar.node.setAttribute(
'aria-label',
this._trans.__('file browser')
);
// File browser widgets container
this.mainPanel = new Panel();
this.mainPanel.addClass(FILE_BROWSER_PANEL_CLASS);
this.mainPanel.title.label = this._trans.__('File Browser');
this.crumbs = new BreadCrumbs({ model, translator });
this.crumbs.addClass(CRUMBS_CLASS);
// The filter toolbar appears immediately below the breadcrumbs and above the directory listing.
const searcher = FilenameSearcher({
updateFilter: (
filterFn: (item: string) => Partial<IScore> | null,
query?: string
) => {
this.model.setFilter(value => {
return filterFn(value.name.toLowerCase());
});
},
useFuzzyFilter: this.model.useFuzzyFilter,
placeholder: this._trans.__('Filter files by name'),
forceRefresh: false,
showIcon: false,
inputRef: this._fileFilterRef,
filterSettingsChanged: this.model.filterSettingsChanged
});
searcher.addClass(FILTERBOX_CLASS);
this.filterToolbar = new Toolbar();
this.filterToolbar.addClass(FILTER_TOOLBAR_CLASS);
this.filterToolbar.node.setAttribute(
'aria-label',
this._trans.__('File browser toolbar')
);
this.filterToolbar.addItem('fileNameSearcher', searcher);
this.filterToolbar.setHidden(!this.showFileFilter);
this.listing = this.createDirListing({
model,
renderer,
translator,
state: options.state,
handleOpenFile: options.handleOpenFile
});
this.listing.addClass(LISTING_CLASS);
this.listing.selectionChanged.connect(() => {
this._selectionChanged.emit();
});
this.mainPanel.addWidget(this.crumbs);
this.mainPanel.addWidget(this.filterToolbar);
this.mainPanel.addWidget(this.listing);
this.addWidget(this.mainPanel);
if (options.restore !== false) {
void model.restore(this.id);
}
// restore listing regardless of the restore option
void this.listing.restore(this.id);
}
/**
* The model used by the file browser.
*/
readonly model: FilterFileBrowserModel;
/**
* Whether to show active file in file browser
*/
get navigateToCurrentDirectory(): boolean {
return this._navigateToCurrentDirectory;
}
set navigateToCurrentDirectory(value: boolean) {
this._navigateToCurrentDirectory = value;
}
/**
* Whether to show the last modified column
*/
get showLastModifiedColumn(): boolean {
return this._showLastModifiedColumn;
}
set showLastModifiedColumn(value: boolean) {
if (this.listing.setColumnVisibility) {
this.listing.setColumnVisibility('last_modified', value);
this._showLastModifiedColumn = value;
} else {
console.warn('Listing does not support toggling column visibility');
}
}
/**
* Number of directory items to show on the left side of the ellipsis in breadcrumbs.
*/
get minimumBreadcrumbsLeftItems(): number {
return this.crumbs.minimumLeftItems;
}
set minimumBreadcrumbsLeftItems(value: number) {
this.crumbs.minimumLeftItems = value;
}
/**
* Number of directory items to show on the right side of the ellipsis in breadcrumbs.
*/
get minimumBreadcrumbsRightItems(): number {
return this.crumbs.minimumRightItems;
}
set minimumBreadcrumbsRightItems(value: number) {
this.crumbs.minimumRightItems = value;
}
/**
* Whether to show the full path in the breadcrumbs
*/
get showFullPath(): boolean {
return this.crumbs.fullPath;
}
set showFullPath(value: boolean) {
this.crumbs.fullPath = value;
}
/**
* Whether to show the file size column
*/
get showFileSizeColumn(): boolean {
return this._showFileSizeColumn;
}
set showFileSizeColumn(value: boolean) {
if (this.listing.setColumnVisibility) {
this.listing.setColumnVisibility('file_size', value);
this._showFileSizeColumn = value;
} else {
console.warn('Listing does not support toggling column visibility');
}
}
/**
* Whether to show hidden files
*/
get showHiddenFiles(): boolean {
return this._showHiddenFiles;
}
set showHiddenFiles(value: boolean) {
this.model.showHiddenFiles(value);
this._showHiddenFiles = value;
}
/**
* Whether to show checkboxes next to files and folders
*/
get showFileCheckboxes(): boolean {
return this._showFileCheckboxes;
}
set showFileCheckboxes(value: boolean) {
if (this.listing.setColumnVisibility) {
this.listing.setColumnVisibility('is_selected', value);
this._showFileCheckboxes = value;
} else {
console.warn('Listing does not support toggling column visibility');
}
}
/**
* Whether to show a text box to filter files by name.
*/
get showFileFilter(): boolean {
return this._showFileFilter;
}
set showFileFilter(value: boolean) {
// If the old value was true and the new value is false, clear the filter
const oldValue = this.showFileFilter;
if (oldValue && !value) {
// Clear the search box input
if (this._fileFilterRef.current) {
this._fileFilterRef.current.value = '';
}
// Set a filter that doesn't exclude anything.
this.model.setFilter(value => {
return {};
});
this.model.refresh().catch(console.warn);
}
this._showFileFilter = value;
// Update widget visibility
this.filterToolbar.setHidden(!this.showFileFilter);
if (this.showFileFilter) {
this._fileFilterRef.current?.focus();
}
}
/**
* Whether to sort notebooks above other files
*/
get sortNotebooksFirst(): boolean {
return this._sortNotebooksFirst;
}
set sortNotebooksFirst(value: boolean) {
if (this.listing.setNotebooksFirstSorting) {
this.listing.setNotebooksFirstSorting(value);
this._sortNotebooksFirst = value;
} else {
console.warn('Listing does not support sorting notebooks first');
}
}
/**
* Whether to allow single click files and directories
*/
get singleClickNavigation(): boolean {
return this._allowSingleClick;
}
set singleClickNavigation(value: boolean) {
if (this.listing.setAllowSingleClickNavigation) {
this.listing.setAllowSingleClickNavigation(value);
this._allowSingleClick = value;
} else {
console.warn('Listing does not support single click navigation');
}
}
/**
* Whether to allow upload of files.
*/
get allowFileUploads(): boolean {
return this._allowFileUploads;
}
set allowFileUploads(value: boolean) {
this.model.allowFileUploads = value;
if (this.listing.setAllowDragDropUpload) {
this.listing.setAllowDragDropUpload(value);
this._allowFileUploads = value;
} else {
console.warn('Listing does not support setting upload');
}
}
/**
* Create an iterator over the listing's selected items.
*
* @returns A new iterator over the listing's selected items.
*/
selectedItems(): IterableIterator<Contents.IModel> {
return this.listing.selectedItems();
}
/**
* A signal emitted when the selection changes in the file browser.
*/
get selectionChanged(): ISignal<this, void> {
return this._selectionChanged;
}
/**
* Select an item by name.
*
* @param name - The name of the item to select.
*/
async selectItemByName(name: string): Promise<void> {
await this.listing.selectItemByName(name);
}
clearSelectedItems(): void {
this.listing.clearSelectedItems();
}
/**
* Rename the first currently selected item.
*
* @returns A promise that resolves with the new name of the item.
*/
rename(): Promise<string> {
return this.listing.rename();
}
/**
* Cut the selected items.
*/
cut(): void {
this.listing.cut();
}
/**
* Copy the selected items.
*/
copy(): void {
this.listing.copy();
}
/**
* Paste the items from the clipboard.
*
* @returns A promise that resolves when the operation is complete.
*/
paste(): Promise<void> {
return this.listing.paste();
}
private async _createNew(
options: Contents.ICreateOptions
): Promise<Contents.IModel> {
// normalize the path if the file is created from a custom drive
if (options.path) {
const localPath = this._manager.services.contents.localPath(options.path);
options.path = this._toDrivePath(this.model.driveName, localPath);
}
try {
const model = await this._manager.newUntitled(options);
await this.listing.selectItemByName(model.name, true);
await this.rename();
return model;
} catch (err) {
void showErrorMessage(this._trans.__('Error'), err);
throw err;
}
}
/**
* Create a new directory
*/
async createNewDirectory(): Promise<Contents.IModel> {
if (this._directoryPending) {
return this._directoryPending;
}
this._directoryPending = this._createNew({
path: this.model.path,
type: 'directory'
});
try {
return await this._directoryPending;
} finally {
this._directoryPending = null;
}
}
/**
* Create a new file
*/
async createNewFile(
options: FileBrowser.IFileOptions
): Promise<Contents.IModel> {
if (this._filePending) {
return this._filePending;
}
this._filePending = this._createNew({
path: this.model.path,
type: 'file',
ext: options.ext
});
try {
return await this._filePending;
} finally {
this._filePending = null;
}
}
/**
* Delete the currently selected item(s).
*
* @returns A promise that resolves when the operation is complete.
*/
delete(): Promise<void> {
return this.listing.delete();
}
/**
* Duplicate the currently selected item(s).
*
* @returns A promise that resolves when the operation is complete.
*/
duplicate(): Promise<void> {
return this.listing.duplicate();
}
/**
* Select all listing items.
*/
selectAll(): Promise<void> {
return this.listing.selectAll();
}
/**
* Download the currently selected item(s).
*/
download(): Promise<void> {
return this.listing.download();
}
/**
* cd ..
*
* Go up one level in the directory tree.
*/
async goUp() {
return this.listing.goUp();
}
/**
* Shut down kernels on the applicable currently selected items.
*
* @returns A promise that resolves when the operation is complete.
*/
shutdownKernels(): Promise<void> {
return this.listing.shutdownKernels();
}
/**
* Select next item.
*/
selectNext(): void {
this.listing.selectNext();
}
/**
* Select previous item.
*/
selectPrevious(): void {
this.listing.selectPrevious();
}
/**
* Find a model given a click.
*
* @param event - The mouse event.
*
* @returns The model for the selected file.
*/
modelForClick(event: MouseEvent): Contents.IModel | undefined {
return this.listing.modelForClick(event);
}
/**
* Create the underlying DirListing instance.
*
* @param options - The DirListing constructor options.
*
* @returns The created DirListing instance.
*/
protected createDirListing(options: DirListing.IOptions): DirListing {
return new DirListing(options);
}
protected translator: ITranslator;
/**
* Handle a connection lost signal from the model.
*/
private _onConnectionFailure(
sender: FilterFileBrowserModel,
args: Error
): void {
if (
args instanceof ServerConnection.ResponseError &&
args.response.status === 404
) {
const title = this._trans.__('Directory not found');
args.message = this._trans.__(
'Directory not found: "%1"',
this.model.path
);
void showErrorMessage(title, args);
}
}
/**
* Given a drive name and a local path, return the full
* drive path which includes the drive name and the local path.
*
* @param driveName the name of the drive
* @param localPath the local path on the drive.
*
* @returns the full drive path
*/
private _toDrivePath(driveName: string, localPath: string): string {
if (driveName === '') {
return localPath;
} else {
return `${driveName}:${PathExt.removeSlash(localPath)}`;
}
}
protected filterToolbar: Toolbar;
protected listing: DirListing;
protected crumbs: BreadCrumbs;
protected mainPanel: Panel;
private _directoryPending: Promise<Contents.IModel> | null = null;
private _filePending: Promise<Contents.IModel> | null = null;
private _fileFilterRef = createRef<HTMLInputElement>();
private _manager: IDocumentManager;
private _navigateToCurrentDirectory: boolean;
private _allowSingleClick: boolean = false;
private _showFileCheckboxes: boolean = false;
private _showFileFilter: boolean = false;
private _showFileSizeColumn: boolean = false;
private _showHiddenFiles: boolean = false;
private _showLastModifiedColumn: boolean = true;
private _sortNotebooksFirst: boolean = false;
private _allowFileUploads: boolean = true;
private _selectionChanged = new Signal<this, void>(this);
}
/**
* The namespace for the `FileBrowser` class statics.
*/
export namespace FileBrowser {
/**
* An options object for initializing a file browser widget.
*/
export interface IOptions {
/**
* The widget/DOM id of the file browser.
*/
id: string;
/**
* A file browser model instance.
*/
model: FilterFileBrowserModel;
/**
* An optional renderer for the directory listing area.
*
* The default is a shared instance of `DirListing.Renderer`.
*/
renderer?: DirListing.IRenderer;
/**
* Whether a file browser automatically restores state when instantiated.
* The default is `true`.
*
* #### Notes
* The file browser model will need to be restored manually for the file
* browser to be able to save its state.
*/
restore?: boolean;
/**
* The application language translator.
*/
translator?: ITranslator;
/**
* An optional state database. If provided, the widget will restore
* the columns sizes
*/
state?: IStateDB;
/**
* Callback overriding action performed when user asks to open a file.
* The default is to open the file in the main area if it is not open already, or to reveal it otherwise.
*/
handleOpenFile?: (path: string) => void;
}
/**
* An options object for creating a file.
*/
export interface IFileOptions {
/**
* The file extension.
*/
ext: string;
}
}