Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export namespace ArrowModel {
rowChunkSize?: number;
colChunkSize?: number;
loadingRepr?: string;
nullRepr?: string;
}
}

Expand All @@ -34,6 +35,7 @@ export class ArrowModel extends DataModel {
rowChunkSize: 512,
colChunkSize: 24,
loadingRepr: "",
nullRepr: "",
...loadingOptions,
};
this._fileOptions = fileOptions;
Expand Down Expand Up @@ -120,7 +122,8 @@ export class ArrowModel extends DataModel {
// We have data
const row_idx_in_chunk = row % this._loadingParams.rowChunkSize;
const col_idx_in_chunk = col % this._loadingParams.colChunkSize;
const out = chunk.getChildAt(col_idx_in_chunk)?.get(row_idx_in_chunk).toString();
const val = chunk.getChildAt(col_idx_in_chunk)?.get(row_idx_in_chunk);
const out = val?.toString() || this._loadingParams.nullRepr;

// Prefetch next chunks only once we have data for the current chunk.
// We chain the Promise because this can be considered a low priority operation so we want
Expand Down
12 changes: 11 additions & 1 deletion src/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export interface FileInfoResponse {

export async function fetchFileInfo(params: Readonly<FileInfoOptions>): Promise<FileInfoResponse> {
const response = await fetch(`/file/info/${params.path}`);
if (!response.ok) {
throw new Error(`Error communicating with the Arbalister server: ${response.status}`);
}
const data: FileInfoResponse = await response.json();
return data;
}
Expand Down Expand Up @@ -83,6 +86,9 @@ export async function fetchStats(
}

const response = await fetch(`/arrow/stats/${params.path}?${query.toString()}`);
if (!response.ok) {
throw new Error(`Error communicating with the Arbalister server: ${response.status}`);
}
const data: StatsResponseRaw = await response.json();

// Validate encoding and content type
Expand Down Expand Up @@ -149,5 +155,9 @@ export async function fetchTable(
}

const url = `/arrow/stream/${params.path}?${query.toString()}`;
return await tableFromIPC(fetch(url));
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Error communicating with the Arbalister server: ${response.status}`);
}
return await tableFromIPC(response);
}
208 changes: 111 additions & 97 deletions src/toolbar.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import { Dialog, showDialog } from "@jupyterlab/apputils";
import { nullTranslator } from "@jupyterlab/translation";
import { Styling } from "@jupyterlab/ui-components";
import { Widget } from "@lumino/widgets";
Expand All @@ -12,44 +10,107 @@ import type {
CsvFileInfo,
CsvReadOptions,
FileInfoFor,
FileReadOptions,
FileReadOptionsFor,
SqliteFileInfo,
SqliteReadOptions,
} from "./file-options";
import type { ArrowGridViewer } from "./widget";

/**
* Base toolbar class for file-specific options with a dropdown selector.
* Base toolbar class for a dropdown selector with error recovery.
* Maintain a value synchronized with the UI and falls back to the previous value on error.
*/
abstract class DropdownToolbar extends Widget {
constructor(gridViewer: ArrowGridViewer, node: HTMLElement) {
constructor(labelName: string, options: Array<[string, string]>, selected: string) {
const node = DropdownToolbar.createDropdownNode(labelName, options, selected);
super({ node });
this._gridViewer = gridViewer;
this._currentValue = selected;
this._labelName = labelName;
this.addClass("arrow-viewer-toolbar");
}

abstract get fileOptions(): FileReadOptions;
/**
* Create a generic dropdown node with a label and options.
*/
protected static createDropdownNode(
labelName: string,
options: Array<[string, string]>,
selected: string,
): HTMLElement {
const div = document.createElement("div");
const label = document.createElement("span");
const select = document.createElement("select");
label.textContent = `${labelName}: `;
label.className = "toolbar-label";
for (const [value, displayLabel] of options) {
const option = document.createElement("option");
option.value = value;
option.textContent = displayLabel;
if (value === selected) {
option.selected = true;
}
select.appendChild(option);
}
div.appendChild(label);
const node = Styling.wrapSelect(select);
node.classList.add("toolbar-dropdown");
div.appendChild(node);
return div;
}

/**
* Called when the dropdown value changes. Implement this to handle the change.
* If this method throws an error, the dropdown will revert to the previous value.
*/
protected abstract onChange(newValue: string): Promise<void>;

protected get value(): string {
return this.selectNode.value;
}

protected set value(val: string) {
this.selectNode.value = val;
}

get selectNode(): HTMLSelectElement {
return this.node.getElementsByTagName("select")![0];
get labelName(): string {
return this._labelName;
}

/**
* Handle the DOM events for the widget.
*
* @param event - The DOM event sent to the widget.
* This method implements the DOM `EventListener` interface and is called in response to events
* on the dock panel's node.
* It should not be called directly by user code.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the dock panel's node. It should
* not be called directly by user code.
* This method is made async to handle the chain of event required to catch the exception and
* show the dialog, but it will be fired and forgotten by the browser.
*
* @param event - The DOM event sent to the widget.
*/
handleEvent(event: Event): void {
async handleEvent(event: Event): Promise<void> {
switch (event.type) {
case "change":
this._gridViewer.updateFileReadOptions(this.fileOptions);
case "change": {
const previousValue = this._currentValue;
const newValue = this.value;
try {
await this.onChange(newValue);
this._currentValue = newValue;
} catch (error) {
// Reset the selector value
this.value = previousValue;

// Show a message to the user
const trans = Dialog.translator.load("jupyterlab");
const cancel = Dialog.cancelButton({ label: trans.__("Close") });
await showDialog({
title: trans.__(`Error changing the ${this.labelName.toLowerCase()} option`),
body: typeof error === "string" ? error : (error as Error).message,
buttons: [cancel],
});
}
break;
}
default:
break;
}
Expand All @@ -63,7 +124,12 @@ abstract class DropdownToolbar extends Widget {
this.selectNode.removeEventListener("change", this);
}

protected _gridViewer: ArrowGridViewer;
private get selectNode(): HTMLSelectElement {
return this.node.getElementsByTagName("select")![0];
}

private _currentValue: string;
private _labelName: string;
}

export namespace CsvToolbar {
Expand All @@ -75,17 +141,25 @@ export namespace CsvToolbar {

export class CsvToolbar extends DropdownToolbar {
constructor(options: CsvToolbar.Options, fileOptions: CsvReadOptions, fileInfo: CsvFileInfo) {
super(
options.gridViewer,
Private.createDelimiterNode(fileOptions.delimiter, fileInfo.delimiters, options.translator),
);
const translator = options.translator || nullTranslator;
const trans = translator.load("jupyterlab");
const delimiterOptions: [string, string][] = fileInfo.delimiters.map((delim) => [delim, delim]);
super(trans.__("Delimiter"), delimiterOptions, fileOptions.delimiter);
this._gridViewer = options.gridViewer;
}

get fileOptions(): CsvReadOptions {
return {
delimiter: this.selectNode.value,
delimiter: this.value,
};
}

protected async onChange(newValue: string): Promise<void> {
this._gridViewer.updateFileReadOptions({ delimiter: newValue });
await this._gridViewer.ready;
}

private _gridViewer: ArrowGridViewer;
}

export namespace SqliteToolbar {
Expand All @@ -101,17 +175,25 @@ export class SqliteToolbar extends DropdownToolbar {
fileOptions: SqliteReadOptions,
fileInfo: SqliteFileInfo,
) {
super(
options.gridViewer,
Private.createTableNameNode(fileOptions.table_name, fileInfo.table_names, options.translator),
);
const translator = options.translator || nullTranslator;
const trans = translator.load("jupyterlab");
const tableOptions: [string, string][] = fileInfo.table_names.map((name) => [name, name]);
super(trans.__("Table"), tableOptions, fileOptions.table_name);
this._gridViewer = options.gridViewer;
}

get fileOptions(): SqliteReadOptions {
return {
table_name: this.selectNode.value,
table_name: this.value,
};
}

protected async onChange(newValue: string): Promise<void> {
this._gridViewer.updateFileReadOptions({ table_name: newValue });
await this._gridViewer.ready;
}

private _gridViewer: ArrowGridViewer;
}

/**
Expand Down Expand Up @@ -145,71 +227,3 @@ export function createToolbar<T extends FileType>(
return null;
}
}

namespace Private {
/**
* Create a labeled dropdown node with items.
*/
function createLabeledDropdown(
label: string,
items: string[],
selected: string,
translator?: ITranslator,
): HTMLElement {
translator = translator || nullTranslator;
const trans = translator?.load("jupyterlab");
const options: [string, string][] = items.map((item) => [item, item]);
return createDropdownNode(trans.__(label), options, selected);
}

/**
* Create the node for the delimiter switcher.
*/
export function createDelimiterNode(
selected: string,
delimiters: string[],
translator?: ITranslator,
): HTMLElement {
return createLabeledDropdown("Delimiter: ", delimiters, selected, translator);
}

/**
* Create the node for the table name switcher.
*/
export function createTableNameNode(
selected: string,
table_names: string[],
translator?: ITranslator,
): HTMLElement {
return createLabeledDropdown("Table: ", table_names, selected, translator);
}

/**
* Create a generic dropdown node with a label and options.
*/
function createDropdownNode(
labelText: string,
options: Array<[string, string]>,
selected: string,
): HTMLElement {
const div = document.createElement("div");
const label = document.createElement("span");
const select = document.createElement("select");
label.textContent = labelText;
label.className = "toolbar-label";
for (const [value, displayLabel] of options) {
const option = document.createElement("option");
option.value = value;
option.textContent = displayLabel;
if (value === selected) {
option.selected = true;
}
select.appendChild(option);
}
div.appendChild(label);
const node = Styling.wrapSelect(select);
node.classList.add("toolbar-dropdown");
div.appendChild(node);
return div;
}
}
2 changes: 1 addition & 1 deletion src/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class ArrowGridViewer extends Panel {
}

get ready(): Promise<void> {
return this._ready;
return this._ready.then(() => this.dataModel.ready);
}

get revealed(): Promise<void> {
Expand Down
Loading