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
15 changes: 15 additions & 0 deletions src/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { isTestExecution } from './common/constants';
import { DebugAdapterNewPtvsd } from './common/experiments/groups';
import { traceError } from './common/logger';
import { IConfigurationService, IExperimentsManager, Resource } from './common/types';
import { IDataViewerDataProvider, IDataViewerFactory } from './datascience/data-viewing/types';
import {
getDebugpyLauncherArgs,
getDebugpyPackagePath,
Expand Down Expand Up @@ -64,6 +65,14 @@ export interface IExtensionApi {
*/
getExecutionCommand(resource?: Resource): string[] | undefined;
};
datascience: {
/**
* Launches Data Viewer component.
* @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data.
* @param {string} title Data Viewer title
*/
showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise<void>;
};
}

export function buildApi(
Expand Down Expand Up @@ -118,6 +127,12 @@ export function buildApi(
// If pythonPath equals an empty string, no interpreter is set.
return pythonPath === '' ? undefined : [pythonPath];
}
},
datascience: {
async showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise<void> {
const dataViewerProviderService = serviceContainer.get<IDataViewerFactory>(IDataViewerFactory);
await dataViewerProviderService.create(dataProvider, title);
}
}
};

Expand Down
85 changes: 47 additions & 38 deletions src/client/datascience/data-viewing/dataViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
'use strict';
import '../../common/extensions';

import { inject, injectable, named } from 'inversify';
import { inject, injectable } from 'inversify';
import * as path from 'path';
import { ViewColumn } from 'vscode';

Expand All @@ -16,28 +16,34 @@ import * as localize from '../../common/utils/localize';
import { noop } from '../../common/utils/misc';
import { StopWatch } from '../../common/utils/stopWatch';
import { sendTelemetryEvent } from '../../telemetry';
import { HelpLinks, Identifiers, Telemetry } from '../constants';
import { HelpLinks, Telemetry } from '../constants';
import { JupyterDataRateLimitError } from '../jupyter/jupyterDataRateLimitError';
import { ICodeCssGenerator, IDataViewer, IJupyterVariable, IJupyterVariables, INotebook, IThemeFinder } from '../types';
import { ICodeCssGenerator, IThemeFinder } from '../types';
import { WebViewHost } from '../webViewHost';
import { DataViewerMessageListener } from './dataViewerMessageListener';
import { DataViewerMessages, IDataViewerMapping, IGetRowsRequest } from './types';
import {
DataViewerMessages,
IDataFrameInfo,
IDataViewer,
IDataViewerDataProvider,
IDataViewerMapping,
IGetRowsRequest
} from './types';

const dataExplorereDir = path.join(EXTENSION_ROOT_DIR, 'out', 'datascience-ui', 'viewers');
@injectable()
export class DataViewer extends WebViewHost<IDataViewerMapping> implements IDataViewer, IDisposable {
private notebook: INotebook | undefined;
private variable: IJupyterVariable | undefined;
private dataProvider: IDataViewerDataProvider | undefined;
private rowsTimer: StopWatch | undefined;
private pendingRowsCount: number = 0;
private dataFrameInfoPromise: Promise<IDataFrameInfo> | undefined;

constructor(
@inject(IWebPanelProvider) provider: IWebPanelProvider,
@inject(IConfigurationService) configuration: IConfigurationService,
@inject(ICodeCssGenerator) cssGenerator: ICodeCssGenerator,
@inject(IThemeFinder) themeFinder: IThemeFinder,
@inject(IWorkspaceService) workspaceService: IWorkspaceService,
@inject(IJupyterVariables) @named(Identifiers.ALL_VARIABLES) private variableManager: IJupyterVariables,
@inject(IApplicationShell) private applicationShell: IApplicationShell,
@inject(IExperimentsManager) experimentsManager: IExperimentsManager,
@inject(UseCustomEditorApi) useCustomEditorApi: boolean
Expand All @@ -57,33 +63,35 @@ export class DataViewer extends WebViewHost<IDataViewerMapping> implements IData
useCustomEditorApi,
false
);

// Load the web panel using our current directory as we don't expect to load any other files
super.loadWebPanel(process.cwd()).catch(traceError);
}

public async showVariable(variable: IJupyterVariable, notebook: INotebook): Promise<void> {
public async showData(dataProvider: IDataViewerDataProvider, title: string): Promise<void> {
if (!this.isDisposed) {
// Save notebook this is tied to
this.notebook = notebook;

// Fill in our variable's beginning data
this.variable = await this.prepVariable(variable, notebook);
// Save the data provider
this.dataProvider = dataProvider;

// Create our new title with the variable name
let newTitle = `${localize.DataScience.dataExplorerTitle()} - ${variable.name}`;
const TRIM_LENGTH = 40;
if (newTitle.length > TRIM_LENGTH) {
newTitle = `${newTitle.substr(0, TRIM_LENGTH)}...`;
}
// Load the web panel using our current directory as we don't expect to load any other files
await super.loadWebPanel(process.cwd()).catch(traceError);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we swallowing errors here. Not sure this is the right thing to do.
How about changing from process.cwd to dataExplorereDir, makes it future proof as well, if we wanted to load soemthing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DonJayamanne great comments :) thanks

  1. Error swallowing: hmm I think it is because this call was done from the constructor, I moved here to make sure the webpanel is loaded before calling setTitle and show. I never realized the error swallowing though, will remove it.

  2. Directory: Good point, just wondering if there is another reason why it was done like that. @rchiodo @IanMatthewHuff any thoughts?


super.setTitle(newTitle);
super.setTitle(title);

// Then show our web panel. Eventually we need to consume the data
await super.show(true);

const dataFrameInfo = await this.prepDataFrameInfo();

// Send a message with our data
this.postMessage(DataViewerMessages.InitializeData, this.variable).ignoreErrors();
this.postMessage(DataViewerMessages.InitializeData, dataFrameInfo).ignoreErrors();
}
}

public dispose(): void {
super.dispose();

if (this.dataProvider) {
// Call dispose on the data provider
this.dataProvider.dispose();
this.dataProvider = undefined;
}
}

Expand All @@ -109,9 +117,16 @@ export class DataViewer extends WebViewHost<IDataViewerMapping> implements IData
super.onMessage(message, payload);
}

private async prepVariable(variable: IJupyterVariable, notebook: INotebook): Promise<IJupyterVariable> {
private getDataFrameInfo(): Promise<IDataFrameInfo> {
if (!this.dataFrameInfoPromise) {
this.dataFrameInfoPromise = this.dataProvider ? this.dataProvider.getDataFrameInfo() : Promise.resolve({});
}
return this.dataFrameInfoPromise;
}

private async prepDataFrameInfo(): Promise<IDataFrameInfo> {
this.rowsTimer = new StopWatch();
const output = await this.variableManager.getDataFrameInfo(variable, notebook);
const output = await this.getDataFrameInfo();

// Log telemetry about number of rows
try {
Expand All @@ -131,13 +146,8 @@ export class DataViewer extends WebViewHost<IDataViewerMapping> implements IData

private async getAllRows() {
return this.wrapRequest(async () => {
if (this.variable && this.variable.rowCount && this.notebook) {
const allRows = await this.variableManager.getDataFrameRows(
this.variable,
this.notebook,
0,
this.variable.rowCount
);
if (this.dataProvider) {
const allRows = await this.dataProvider.getAllRows();
this.pendingRowsCount = 0;
return this.postMessage(DataViewerMessages.GetAllRowsResponse, allRows);
}
Expand All @@ -146,12 +156,11 @@ export class DataViewer extends WebViewHost<IDataViewerMapping> implements IData

private getRowChunk(request: IGetRowsRequest) {
return this.wrapRequest(async () => {
if (this.variable && this.variable.rowCount && this.notebook) {
const rows = await this.variableManager.getDataFrameRows(
this.variable,
this.notebook,
if (this.dataProvider) {
const dataFrameInfo = await this.getDataFrameInfo();
const rows = await this.dataProvider.getRows(
request.start,
Math.min(request.end, this.variable.rowCount)
Math.min(request.end, dataFrameInfo.rowCount ? dataFrameInfo.rowCount : 0)
);
return this.postMessage(DataViewerMessages.GetRowsResponse, {
rows,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,14 @@ import { inject, injectable } from 'inversify';

import { IAsyncDisposable, IAsyncDisposableRegistry } from '../../common/types';
import { IServiceContainer } from '../../ioc/types';
import { IDataViewer, IDataViewerProvider, IJupyterVariable, INotebook } from '../types';
import { DataViewerDependencyService } from './dataViewerDependencyService';
import { IDataViewer, IDataViewerDataProvider, IDataViewerFactory } from './types';

@injectable()
export class DataViewerProvider implements IDataViewerProvider, IAsyncDisposable {
export class DataViewerFactory implements IDataViewerFactory, IAsyncDisposable {
private activeExplorers: IDataViewer[] = [];
constructor(
@inject(IServiceContainer) private serviceContainer: IServiceContainer,
@inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry,
@inject(DataViewerDependencyService) private dependencyService: DataViewerDependencyService
@inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry
) {
asyncRegistry.push(this);
}
Expand All @@ -25,18 +23,17 @@ export class DataViewerProvider implements IDataViewerProvider, IAsyncDisposable
await Promise.all(this.activeExplorers.map((d) => d.dispose()));
}

public async create(variable: IJupyterVariable, notebook: INotebook): Promise<IDataViewer> {
public async create(dataProvider: IDataViewerDataProvider, title: string): Promise<IDataViewer> {
let result: IDataViewer | undefined;

// Create the data explorer (this should show the window)
// Create the data explorer
const dataExplorer = this.serviceContainer.get<IDataViewer>(IDataViewer);
try {
// Verify this is allowed.
await this.dependencyService.checkAndInstallMissingDependencies(notebook.getMatchingInterpreter());

// Then load the data.
this.activeExplorers.push(dataExplorer);
await dataExplorer.showVariable(variable, notebook);

// Show the window and the data
await dataExplorer.showData(dataProvider, title);
result = dataExplorer;
} finally {
if (!result) {
Expand Down
113 changes: 113 additions & 0 deletions src/client/datascience/data-viewing/jupyterVariableDataProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
import '../../common/extensions';

import { inject, injectable, named } from 'inversify';

import { Identifiers } from '../constants';
import { IJupyterVariable, IJupyterVariableDataProvider, IJupyterVariables, INotebook } from '../types';
import { DataViewerDependencyService } from './dataViewerDependencyService';
import { ColumnType, IDataFrameInfo, IRowsResponse } from './types';

@injectable()
export class JupyterVariableDataProvider implements IJupyterVariableDataProvider {
private initialized: boolean = false;
private notebook: INotebook | undefined;
private variable: IJupyterVariable | undefined;

constructor(
@inject(IJupyterVariables) @named(Identifiers.ALL_VARIABLES) private variableManager: IJupyterVariables,
@inject(DataViewerDependencyService) private dependencyService: DataViewerDependencyService
) {}

/**
* Normalizes column types to the types the UI component understands.
* Defaults to 'string'.
* @param columns
* @returns Array of columns with normalized type
*/
private static getNormalizedColumns(columns: { key: string; type: string }[]): { key: string; type: ColumnType }[] {
return columns.map((column: { key: string; type: string }) => {
let normalizedType: ColumnType;
switch (column.type) {
case 'bool':
normalizedType = ColumnType.Bool;
break;
case 'integer':
case 'int32':
case 'int64':
case 'float':
case 'float32':
case 'float64':
case 'number':
normalizedType = ColumnType.Number;
break;
default:
normalizedType = ColumnType.String;
}
return {
key: column.key,
type: normalizedType
};
});
}

public dispose(): void {
return;
}

public setDependencies(variable: IJupyterVariable, notebook: INotebook): void {
this.notebook = notebook;
this.variable = variable;
}

public async getDataFrameInfo(): Promise<IDataFrameInfo> {
let dataFrameInfo: IDataFrameInfo = {};
await this.ensureInitialized();
if (this.variable && this.notebook) {
dataFrameInfo = {
columns: this.variable.columns
? JupyterVariableDataProvider.getNormalizedColumns(this.variable.columns)
: this.variable.columns,
indexColumn: this.variable.indexColumn,
rowCount: this.variable.rowCount
};
}
return dataFrameInfo;
}

public async getAllRows() {
let allRows: IRowsResponse = [];
await this.ensureInitialized();
if (this.variable && this.variable.rowCount && this.notebook) {
const dataFrameRows = await this.variableManager.getDataFrameRows(
this.variable,
this.notebook,
0,
this.variable.rowCount
);
allRows = dataFrameRows && dataFrameRows.data ? (dataFrameRows.data as IRowsResponse) : [];
}
return allRows;
}

public async getRows(start: number, end: number) {
let rows: IRowsResponse = [];
await this.ensureInitialized();
if (this.variable && this.variable.rowCount && this.notebook) {
const dataFrameRows = await this.variableManager.getDataFrameRows(this.variable, this.notebook, start, end);
rows = dataFrameRows && dataFrameRows.data ? (dataFrameRows.data as IRowsResponse) : [];
}
return rows;
}

private async ensureInitialized(): Promise<void> {
// Postpone pre-req and variable initialization until data is requested.
if (!this.initialized && this.variable && this.notebook) {
this.initialized = true;
await this.dependencyService.checkAndInstallMissingDependencies(this.notebook.getMatchingInterpreter());
this.variable = await this.variableManager.getDataFrameInfo(this.variable, this.notebook);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
import '../../common/extensions';

import { inject, injectable } from 'inversify';

import { IServiceContainer } from '../../ioc/types';
import {
IJupyterVariable,
IJupyterVariableDataProvider,
IJupyterVariableDataProviderFactory,
INotebook
} from '../types';

@injectable()
export class JupyterVariableDataProviderFactory implements IJupyterVariableDataProviderFactory {
constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {}

public async create(variable: IJupyterVariable, notebook: INotebook): Promise<IJupyterVariableDataProvider> {
const jupyterVariableDataProvider = this.serviceContainer.get<IJupyterVariableDataProvider>(
IJupyterVariableDataProvider
);
jupyterVariableDataProvider.setDependencies(variable, notebook);
return jupyterVariableDataProvider;
}
}
Loading