refactor notebook to be fileservice based (#6459)

This commit is contained in:
Anthony Dresser
2019-07-26 22:19:13 -07:00
committed by GitHub
parent 8e40aa3306
commit 371504358d
80 changed files with 192 additions and 175 deletions

View File

@@ -0,0 +1,184 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ElementRef } from '@angular/core';
import { localize } from 'vs/nls';
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { getErrorMessage } from 'vs/base/common/errors';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import * as DOM from 'vs/base/browser/dom';
import { INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
import { CellActionBase, CellContext } from 'sql/workbench/parts/notebook/browser/cellViews/codeActions';
import { CellTypes, CellType } from 'sql/workbench/parts/notebook/common/models/contracts';
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import { ToggleMoreWidgetAction } from 'sql/workbench/parts/dashboard/browser/core/actions';
import { CellModel } from 'sql/workbench/parts/notebook/common/models/cell';
export const HIDDEN_CLASS = 'actionhidden';
export class CellToggleMoreActions {
private _actions: CellActionBase[] = [];
private _moreActions: ActionBar;
private _moreActionsElement: HTMLElement;
constructor(
@IInstantiationService private instantiationService: IInstantiationService) {
this._actions.push(
instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', "Delete")),
instantiationService.createInstance(AddCellFromContextAction, 'codeBefore', localize('codeBefore', "Insert Code Before"), CellTypes.Code, false),
instantiationService.createInstance(AddCellFromContextAction, 'codeAfter', localize('codeAfter', "Insert Code After"), CellTypes.Code, true),
instantiationService.createInstance(AddCellFromContextAction, 'markdownBefore', localize('markdownBefore', "Insert Text Before"), CellTypes.Markdown, false),
instantiationService.createInstance(AddCellFromContextAction, 'markdownAfter', localize('markdownAfter', "Insert Text After"), CellTypes.Markdown, true),
instantiationService.createInstance(RunCellsAction, 'runAllBefore', localize('runAllBefore', "Run Cells Before"), false),
instantiationService.createInstance(RunCellsAction, 'runAllAfter', localize('runAllAfter', "Run Cells After"), true),
instantiationService.createInstance(ClearCellOutputAction, 'clear', localize('clear', "Clear Output"))
);
}
public onInit(elementRef: ElementRef, model: NotebookModel, cellModel: ICellModel) {
let context = new CellContext(model, cellModel);
this._moreActionsElement = <HTMLElement>elementRef.nativeElement;
if (this._moreActionsElement.childNodes.length > 0) {
this._moreActionsElement.removeChild(this._moreActionsElement.childNodes[0]);
}
this._moreActions = new ActionBar(this._moreActionsElement, { orientation: ActionsOrientation.VERTICAL });
this._moreActions.context = { target: this._moreActionsElement };
let validActions = this._actions.filter(a => a.canRun(context));
this._moreActions.push(this.instantiationService.createInstance(ToggleMoreWidgetAction, validActions, context), { icon: true, label: false });
}
public toggleVisible(visible: boolean): void {
if (!this._moreActionsElement) {
return;
}
if (visible) {
DOM.addClass(this._moreActionsElement, HIDDEN_CLASS);
} else {
DOM.removeClass(this._moreActionsElement, HIDDEN_CLASS);
}
}
}
export class AddCellFromContextAction extends CellActionBase {
constructor(
id: string, label: string, private cellType: CellType, private isAfter: boolean,
@INotificationService notificationService: INotificationService
) {
super(id, label, undefined, notificationService);
}
doRun(context: CellContext): Promise<void> {
try {
let model = context.model;
let index = model.cells.findIndex((cell) => cell.id === context.cell.id);
if (index !== undefined && this.isAfter) {
index += 1;
}
model.addCell(this.cellType, index);
} catch (error) {
let message = getErrorMessage(error);
this.notificationService.notify({
severity: Severity.Error,
message: message
});
}
return Promise.resolve();
}
}
export class DeleteCellAction extends CellActionBase {
constructor(id: string, label: string,
@INotificationService notificationService: INotificationService
) {
super(id, label, undefined, notificationService);
}
doRun(context: CellContext): Promise<void> {
try {
context.model.deleteCell(context.cell);
} catch (error) {
let message = getErrorMessage(error);
this.notificationService.notify({
severity: Severity.Error,
message: message
});
}
return Promise.resolve();
}
}
export class ClearCellOutputAction extends CellActionBase {
constructor(id: string, label: string,
@INotificationService notificationService: INotificationService
) {
super(id, label, undefined, notificationService);
}
public canRun(context: CellContext): boolean {
return context.cell && context.cell.cellType === CellTypes.Code;
}
doRun(context: CellContext): Promise<void> {
try {
let cell = context.cell || context.model.activeCell;
if (cell) {
(cell as CellModel).clearOutputs();
}
} catch (error) {
let message = getErrorMessage(error);
this.notificationService.notify({
severity: Severity.Error,
message: message
});
}
return Promise.resolve();
}
}
export class RunCellsAction extends CellActionBase {
constructor(id: string,
label: string,
private isAfter: boolean,
@INotificationService notificationService: INotificationService,
@INotebookService private notebookService: INotebookService,
) {
super(id, label, undefined, notificationService);
}
public canRun(context: CellContext): boolean {
return context.cell && context.cell.cellType === CellTypes.Code;
}
async doRun(context: CellContext): Promise<void> {
try {
let cell = context.cell || context.model.activeCell;
if (cell) {
let editor = this.notebookService.findNotebookEditor(cell.notebookModel.notebookUri);
if (editor) {
if (this.isAfter) {
await editor.runAllCells(cell, undefined);
} else {
await editor.runAllCells(undefined, cell);
}
}
}
} catch (error) {
let message = getErrorMessage(error);
this.notificationService.notify({
severity: Severity.Error,
message: message
});
}
return Promise.resolve();
}
}

View File

@@ -0,0 +1,14 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div style="width: 100%; height: 100%; display: flex; flex-flow: row" (mouseover)="hover=true" (mouseleave)="hover=false">
<div #toolbar class="toolbar">
</div>
<div #editor class="editor" style="flex: 1 1 auto; overflow: hidden;">
</div>
<div #moreactions class="moreActions" style="flex: 0 0 auto; display: flex; flex-flow:column;width: 20px; min-height: 20px; max-height: 20px; padding-top: 0px; orientation: portrait">
</div>
</div>

View File

@@ -0,0 +1,326 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./code';
import { OnInit, Component, Input, Inject, ElementRef, ViewChild, Output, EventEmitter, OnChanges, SimpleChange, forwardRef, ChangeDetectorRef } from '@angular/core';
import { AngularDisposable } from 'sql/base/browser/lifecycle';
import { QueryTextEditor } from 'sql/workbench/browser/modelComponents/queryTextEditor';
import { CellToggleMoreActions } from 'sql/workbench/parts/notebook/browser/cellToggleMoreActions';
import { ICellModel, notebookConstants, CellExecutionState } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { RunCellAction, CellContext } from 'sql/workbench/parts/notebook/browser/cellViews/codeActions';
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import * as themeColors from 'vs/workbench/common/theme';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { SimpleEditorProgressService } from 'vs/editor/standalone/browser/simpleServices';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITextModel } from 'vs/editor/common/model';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import * as DOM from 'vs/base/browser/dom';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { Event, Emitter } from 'vs/base/common/event';
import { CellTypes } from 'sql/workbench/parts/notebook/common/models/contracts';
import { OVERRIDE_EDITOR_THEMING_SETTING } from 'sql/workbench/services/notebook/common/notebookService';
import * as notebookUtils from 'sql/workbench/parts/notebook/common/models/notebookUtils';
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { ILogService } from 'vs/platform/log/common/log';
export const CODE_SELECTOR: string = 'code-component';
const MARKDOWN_CLASS = 'markdown';
@Component({
selector: CODE_SELECTOR,
templateUrl: decodeURI(require.toUrl('./code.component.html'))
})
export class CodeComponent extends AngularDisposable implements OnInit, OnChanges {
@ViewChild('toolbar', { read: ElementRef }) private toolbarElement: ElementRef;
@ViewChild('moreactions', { read: ElementRef }) private moreActionsElementRef: ElementRef;
@ViewChild('editor', { read: ElementRef }) private codeElement: ElementRef;
public get cellModel(): ICellModel {
return this._cellModel;
}
@Input() public set cellModel(value: ICellModel) {
this._cellModel = value;
if (this.toolbarElement && value && value.cellType === CellTypes.Markdown) {
let nativeToolbar = <HTMLElement>this.toolbarElement.nativeElement;
DOM.addClass(nativeToolbar, MARKDOWN_CLASS);
}
}
@Output() public onContentChanged = new EventEmitter<void>();
@Input() set model(value: NotebookModel) {
this._model = value;
this._register(value.kernelChanged(() => {
// On kernel change, need to reevaluate the language for each cell
// Refresh based on the cell magic (since this is kernel-dependent) and then update using notebook language
this.checkForLanguageMagics();
this.updateLanguageMode();
}));
this._register(value.onValidConnectionSelected(() => {
this.updateConnectionState(this.isActive());
}));
}
@Input() set activeCellId(value: string) {
this._activeCellId = value;
}
@Input() set hover(value: boolean) {
this.cellModel.hover = value;
if (!this.isActive()) {
// Only make a change if we're not active, since this has priority
this.toggleMoreActionsButton(this.cellModel.hover);
}
}
protected _actionBar: Taskbar;
private readonly _minimumHeight = 30;
private readonly _maximumHeight = 4000;
private _cellModel: ICellModel;
private _editor: QueryTextEditor;
private _editorInput: UntitledEditorInput;
private _editorModel: ITextModel;
private _model: NotebookModel;
private _activeCellId: string;
private _cellToggleMoreActions: CellToggleMoreActions;
private _layoutEmitter = new Emitter<void>();
constructor(
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
@Inject(IModelService) private _modelService: IModelService,
@Inject(IModeService) private _modeService: IModeService,
@Inject(IConfigurationService) private _configurationService: IConfigurationService,
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(ILogService) private readonly logService: ILogService
) {
super();
this._cellToggleMoreActions = this._instantiationService.createInstance(CellToggleMoreActions);
this._register(Event.debounce(this._layoutEmitter.event, (l, e) => e, 250, /*leading=*/false)
(() => this.layout()));
// Handle disconnect on removal of the cell, if it was the active cell
this._register({ dispose: () => this.updateConnectionState(false) });
}
ngOnInit() {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
this.initActionBar();
}
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
this.updateLanguageMode();
this.updateModel();
for (let propName in changes) {
if (propName === 'activeCellId') {
let changedProp = changes[propName];
let isActive = this.cellModel.id === changedProp.currentValue;
this.updateConnectionState(isActive);
this.toggleMoreActionsButton(isActive);
if (this._editor) {
this._editor.toggleEditorSelected(isActive);
}
break;
}
}
}
private updateConnectionState(shouldConnect: boolean) {
if (this.isSqlCodeCell()) {
let cellUri = this.cellModel.cellUri.toString();
let connectionService = this.connectionService;
if (!shouldConnect && connectionService && connectionService.isConnected(cellUri)) {
connectionService.disconnect(cellUri).catch(e => this.logService.error(e));
} else if (shouldConnect && this._model.activeConnection && this._model.activeConnection.id !== '-1') {
connectionService.connect(this._model.activeConnection, cellUri).catch(e => this.logService.error(e));
}
}
}
private get connectionService(): IConnectionManagementService {
return this._model && this._model.notebookOptions && this._model.notebookOptions.connectionService;
}
private isSqlCodeCell() {
return this._model
&& this._model.defaultKernel
&& this._model.defaultKernel.display_name === notebookConstants.SQL
&& this.cellModel.cellType === CellTypes.Code
&& this.cellModel.cellUri;
}
private get destroyed(): boolean {
return !!(this._changeRef['destroyed']);
}
ngAfterContentInit(): void {
if (this.destroyed) {
return;
}
this.createEditor();
this._register(DOM.addDisposableListener(window, DOM.EventType.RESIZE, e => {
this._layoutEmitter.fire();
}));
}
ngAfterViewInit(): void {
this._layoutEmitter.fire();
}
get model(): NotebookModel {
return this._model;
}
get activeCellId(): string {
return this._activeCellId;
}
private async createEditor(): Promise<void> {
let instantiationService = this._instantiationService.createChild(new ServiceCollection([IProgressService, new SimpleEditorProgressService()]));
this._editor = instantiationService.createInstance(QueryTextEditor);
this._editor.create(this.codeElement.nativeElement);
this._editor.setVisible(true);
this._editor.setMinimumHeight(this._minimumHeight);
this._editor.setMaximumHeight(this._maximumHeight);
let uri = this.cellModel.cellUri;
let cellModelSource: string;
cellModelSource = Array.isArray(this.cellModel.source) ? this.cellModel.source.join('') : this.cellModel.source;
this._editorInput = instantiationService.createInstance(UntitledEditorInput, uri, false, this.cellModel.language, cellModelSource, '');
await this._editor.setInput(this._editorInput, undefined);
this.setFocusAndScroll();
let untitledEditorModel: UntitledEditorModel = await this._editorInput.resolve();
this._editorModel = untitledEditorModel.textEditorModel;
let isActive = this.cellModel.id === this._activeCellId;
this._editor.toggleEditorSelected(isActive);
// For markdown cells, don't show line numbers unless we're using editor defaults
let overrideEditorSetting = this._configurationService.getValue<boolean>(OVERRIDE_EDITOR_THEMING_SETTING);
this._editor.hideLineNumbers = (overrideEditorSetting && this.cellModel.cellType === CellTypes.Markdown);
if (this.destroyed) {
// At this point, we may have been disposed (scenario: restoring markdown cell in preview mode).
// Exiting early to avoid warnings on registering already disposed items, which causes some churning
// due to re-disposing things.
// There's no negative impact as at this point the component isn't visible (it was removed from the DOM)
return;
}
this._register(this._editor);
this._register(this._editorInput);
this._register(this._editorModel.onDidChangeContent(e => {
this._editor.setHeightToScrollHeight();
this.cellModel.source = this._editorModel.getValue();
this.onContentChanged.emit();
this.checkForLanguageMagics();
// TODO see if there's a better way to handle reassessing size.
setTimeout(() => this._layoutEmitter.fire(), 250);
}));
this._register(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('editor.wordWrap') || e.affectsConfiguration('editor.fontSize')) {
this._editor.setHeightToScrollHeight(true);
}
}));
this._register(this.model.layoutChanged(() => this._layoutEmitter.fire(), this));
this._register(this.cellModel.onExecutionStateChange(event => {
if (event === CellExecutionState.Running && !this.cellModel.stdInVisible) {
this.setFocusAndScroll();
}
}));
this.layout();
}
public layout(): void {
this._editor.layout(new DOM.Dimension(
DOM.getContentWidth(this.codeElement.nativeElement),
DOM.getContentHeight(this.codeElement.nativeElement)));
this._editor.setHeightToScrollHeight();
}
protected initActionBar() {
let context = new CellContext(this.model, this.cellModel);
let runCellAction = this._instantiationService.createInstance(RunCellAction, context);
let taskbar = <HTMLElement>this.toolbarElement.nativeElement;
this._actionBar = new Taskbar(taskbar);
this._actionBar.context = context;
this._actionBar.setContent([
{ action: runCellAction }
]);
this._cellToggleMoreActions.onInit(this.moreActionsElementRef, this.model, this.cellModel);
}
/// Editor Functions
private updateModel() {
if (this._editorModel) {
let cellModelSource: string;
cellModelSource = Array.isArray(this.cellModel.source) ? this.cellModel.source.join('') : this.cellModel.source;
this._modelService.updateModel(this._editorModel, cellModelSource);
}
}
private checkForLanguageMagics(): void {
try {
if (!this.cellModel || this.cellModel.cellType !== CellTypes.Code) {
return;
}
if (this._editorModel && this._editor && this._editorModel.getLineCount() > 1) {
// Only try to match once we've typed past the first line
let magicName = notebookUtils.tryMatchCellMagic(this._editorModel.getLineContent(1));
if (magicName) {
let kernelName = this._model.clientSession && this._model.clientSession.kernel ? this._model.clientSession.kernel.name : undefined;
let magic = this._model.notebookOptions.cellMagicMapper.toLanguageMagic(magicName, kernelName);
if (magic && this.cellModel.language !== magic.language) {
this.cellModel.setOverrideLanguage(magic.language);
this.updateLanguageMode();
}
} else {
this.cellModel.setOverrideLanguage(undefined);
}
}
} catch (err) {
// No-op for now. Should we log?
}
}
private updateLanguageMode(): void {
if (this._editorModel && this._editor) {
let modeValue = this._modeService.create(this.cellModel.language);
this._modelService.setMode(this._editorModel, modeValue);
}
}
private updateTheme(theme: IColorTheme): void {
let toolbarEl = <HTMLElement>this.toolbarElement.nativeElement;
toolbarEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
let moreActionsEl = <HTMLElement>this.moreActionsElementRef.nativeElement;
moreActionsEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
}
private setFocusAndScroll(): void {
if (this.cellModel.id === this._activeCellId) {
this._editor.focus();
this._editor.getContainer().scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
protected isActive() {
return this.cellModel && this.cellModel.id === this.activeCellId;
}
protected toggleMoreActionsButton(isActiveOrHovered: boolean) {
this._cellToggleMoreActions.toggleVisible(!isActiveOrHovered);
}
}

View File

@@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
code-component {
height: 100%;
width: 100%;
display: block;
}
code-component .toolbar {
border-right-width: 1px;
flex: 0 0 auto;
display: flex;
flex-flow:column;
width: 40px;
min-height: 40px;
orientation: portrait
}
code-component .toolbar.markdown {
display: none;
}
code-component .toolbar .carbon-taskbar {
position: sticky;
top: 0px;
margin-top: 5px;
}
code-component .toolbarIconRun {
height: 20px;
background-image: url('./media/light/execute_cell.svg');
padding-bottom: 10px;
}
.vs-dark code-component .toolbarIconRun,
.hc-black code-component .toolbarIconRun {
background-image: url('./media/dark/execute_cell_inverse.svg');
}
code-component .toolbarIconRunError {
height: 20px;
background-image: url('./media/light/execute_cell_error.svg');
padding-bottom: 10px;
}
code-component .toolbarIconStop {
height: 20px;
background-image: url('./media/light/stop_cell_solidanimation.svg');
padding-bottom: 10px;
}
.vs-dark code-component .toolbarIconStop,
.hc-black code-component .toolbarIconStop {
background-image: url('./media/dark/stop_cell_solidanimation_inverse.svg');
}
code-component .editor {
padding: 5px 0px 5px 0px
}
/* overview ruler */
code-component .monaco-editor .decorationsOverviewRuler {
visibility: hidden;
}
code-component .carbon-taskbar .icon {
background-size: 20px;
width: 40px;
}
code-component .carbon-taskbar .icon.hideIcon {
width: 0px;
padding-left: 0px;
padding-top: 6px;
font-family: monospace;
font-size: 12px;
}
code-component .carbon-taskbar .icon.hideIcon.execCountTen {
margin-left: -2px;
}
code-component .carbon-taskbar .icon.hideIcon.execCountHundred {
margin-left: -6px;
}
code-component .carbon-taskbar.monaco-toolbar .monaco-action-bar.animated .actions-container
{
padding-left: 10px
}

View File

@@ -0,0 +1,135 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Action } from 'vs/base/common/actions';
import { localize } from 'vs/nls';
import { IDisposable } from 'vs/base/common/lifecycle';
import * as types from 'vs/base/common/types';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
import { ICellModel, CellExecutionState } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { MultiStateAction, IMultiStateData } from 'sql/workbench/parts/notebook/browser/notebookActions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ILogService } from 'vs/platform/log/common/log';
import { getErrorMessage } from 'vs/base/common/errors';
let notebookMoreActionMsg = localize('notebook.failed', "Please select active cell and try again");
const emptyExecutionCountLabel = '[ ]';
function hasModelAndCell(context: CellContext, notificationService: INotificationService): boolean {
if (!context || !context.model) {
return false;
}
if (context.cell === undefined) {
notificationService.notify({
severity: Severity.Error,
message: notebookMoreActionMsg
});
return false;
}
return true;
}
export class CellContext {
constructor(public model: NotebookModel, private _cell?: ICellModel) {
}
public get cell(): ICellModel {
return this._cell ? this._cell : this.model.activeCell;
}
}
export abstract class CellActionBase extends Action {
constructor(id: string, label: string, icon: string, protected notificationService: INotificationService) {
super(id, label, icon);
}
public canRun(context: CellContext): boolean {
return true;
}
public run(context: CellContext): Promise<boolean> {
if (hasModelAndCell(context, this.notificationService)) {
return this.doRun(context).then(() => true);
}
return Promise.resolve(true);
}
abstract doRun(context: CellContext): Promise<void>;
}
export class RunCellAction extends MultiStateAction<CellExecutionState> {
public static ID = 'notebook.runCell';
public static LABEL = 'Run cell';
private _executionChangedDisposable: IDisposable;
private _context: CellContext;
constructor(context: CellContext, @INotificationService private notificationService: INotificationService,
@IConnectionManagementService private connectionManagementService: IConnectionManagementService,
@IKeybindingService keybindingService: IKeybindingService,
@ILogService logService: ILogService
) {
super(RunCellAction.ID, new IMultiStateData<CellExecutionState>([
{ key: CellExecutionState.Hidden, value: { label: emptyExecutionCountLabel, className: '', tooltip: '', hideIcon: true } },
{ key: CellExecutionState.Stopped, value: { label: '', className: 'toolbarIconRun', tooltip: localize('runCell', "Run cell"), commandId: 'notebook.command.runactivecell' } },
{ key: CellExecutionState.Running, value: { label: '', className: 'toolbarIconStop', tooltip: localize('stopCell', "Cancel execution") } },
{ key: CellExecutionState.Error, value: { label: '', className: 'toolbarIconRunError', tooltip: localize('errorRunCell', "Error on last run. Click to run again") } },
], CellExecutionState.Hidden), keybindingService, logService);
this.ensureContextIsUpdated(context);
}
public run(context?: CellContext): Promise<boolean> {
return this.doRun(context).then(() => true);
}
public async doRun(context: CellContext): Promise<void> {
this.ensureContextIsUpdated(context);
if (!this._context) {
// TODO should we error?
return;
}
try {
await this._context.cell.runCell(this.notificationService, this.connectionManagementService);
} catch (error) {
let message = getErrorMessage(error);
this.notificationService.error(message);
}
}
private ensureContextIsUpdated(context: CellContext) {
if (context && context !== this._context) {
if (this._executionChangedDisposable) {
this._executionChangedDisposable.dispose();
}
this._context = context;
this.updateStateAndExecutionCount(context.cell.executionState);
this._executionChangedDisposable = this._context.cell.onExecutionStateChange((state) => {
this.updateStateAndExecutionCount(state);
});
}
}
private updateStateAndExecutionCount(state: CellExecutionState) {
let label = emptyExecutionCountLabel;
let className = '';
if (!types.isUndefinedOrNull(this._context.cell.executionCount)) {
label = `[${this._context.cell.executionCount}]`;
// Heuristic to try and align correctly independent of execution count length. Moving left margin
// back by a few px seems to make things "work" OK, but isn't a super clean solution
if (label.length === 4) {
className = 'execCountTen';
} else if (label.length > 4) {
className = 'execCountHundred';
}
}
this.states.updateStateData(CellExecutionState.Hidden, (data) => {
data.label = label;
data.className = className;
});
this.updateState(state);
}
}

View File

@@ -0,0 +1,16 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div style="width: 100%; height: 100%; display: flex; flex-flow: column">
<div style="flex: 0 0 auto;">
<code-component [cellModel]="cellModel" [model]="model" [activeCellId]="activeCellId"></code-component>
</div>
<div style="flex: 0 0 auto; width: 100%; height: 100%; display: block">
<output-area-component *ngIf="cellModel.outputs && cellModel.outputs.length > 0" [cellModel]="cellModel" [activeCellId]="activeCellId">
</output-area-component>
<stdin-component *ngIf="isStdInVisible" [onSendInput]="inputDeferred" [stdIn]="stdIn" [cellModel]="cellModel"></stdin-component>
</div>
</div>

View File

@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { nb } from 'azdata';
import { OnInit, Component, Input, Inject, forwardRef, ChangeDetectorRef, SimpleChange, OnChanges, HostListener } from '@angular/core';
import { CellView } from 'sql/workbench/parts/notebook/browser/cellViews/interfaces';
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
import { Deferred } from 'sql/base/common/promise';
export const CODE_SELECTOR: string = 'code-cell-component';
@Component({
selector: CODE_SELECTOR,
templateUrl: decodeURI(require.toUrl('./codeCell.component.html'))
})
export class CodeCellComponent extends CellView implements OnInit, OnChanges {
@Input() cellModel: ICellModel;
@Input() set model(value: NotebookModel) {
this._model = value;
}
@Input() set activeCellId(value: string) {
this._activeCellId = value;
}
@HostListener('document:keydown.escape', ['$event'])
handleKeyboardEvent() {
this.cellModel.active = false;
this._model.activeCell = undefined;
}
private _model: NotebookModel;
private _activeCellId: string;
public inputDeferred: Deferred<string>;
public stdIn: nb.IStdinMessage;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
) {
super();
}
ngOnInit() {
if (this.cellModel) {
this._register(this.cellModel.onOutputsChanged(() => {
this._changeRef.detectChanges();
}));
// Register request handler, cleanup on dispose of this component
this.cellModel.setStdInHandler({ handle: (msg) => this.handleStdIn(msg) });
this._register({ dispose: () => this.cellModel.setStdInHandler(undefined) });
}
}
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
for (let propName in changes) {
if (propName === 'activeCellId') {
let changedProp = changes[propName];
this._activeCellId = changedProp.currentValue;
break;
}
}
}
get model(): NotebookModel {
return this._model;
}
get activeCellId(): string {
return this._activeCellId;
}
public layout() {
}
handleStdIn(msg: nb.IStdinMessage): void | Thenable<void> {
if (msg) {
this.stdIn = msg;
this.inputDeferred = new Deferred();
this.cellModel.stdInVisible = true;
this._changeRef.detectChanges();
return this.awaitStdIn();
}
}
private async awaitStdIn(): Promise<void> {
try {
let value = await this.inputDeferred.promise;
this.cellModel.future.sendInputReply({ value: value });
} catch (err) {
// Note: don't have a better way to handle completing input request. For now just canceling by sending empty string?
this.cellModel.future.sendInputReply({ value: '' });
} finally {
// Clean up so no matter what, the stdIn request goes away
this.stdIn = undefined;
this.inputDeferred = undefined;
this.cellModel.stdInVisible = false;
this._changeRef.detectChanges();
}
}
get isStdInVisible(): boolean {
return this.cellModel.stdInVisible;
}
}

View File

@@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { OnDestroy } from '@angular/core';
import { AngularDisposable } from 'sql/base/browser/lifecycle';
export abstract class CellView extends AngularDisposable implements OnDestroy {
constructor() {
super();
}
public abstract layout(): void;
}

View File

@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Directive, Inject, HostListener, Input } from '@angular/core';
import * as types from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { onUnexpectedError } from 'vs/base/common/errors';
import product from 'vs/platform/product/node/product';
import { INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
const knownSchemes = new Set(['http', 'https', 'file', 'mailto', 'data', `${product.urlProtocol}`, 'azuredatastudio', 'azuredatastudio-insiders', 'vscode', 'vscode-insiders', 'vscode-resource']);
@Directive({
selector: '[link-handler]',
})
export class LinkHandlerDirective {
private workbenchFilePath: URI;
@Input() isTrusted: boolean;
@Input() notebookUri: URI;
constructor(
@Inject(IOpenerService) private readonly openerService: IOpenerService,
@Inject(INotebookService) private readonly notebookService: INotebookService
) {
this.workbenchFilePath = URI.parse(require.toUrl('vs/code/electron-browser/workbench/workbench.html'));
}
@HostListener('click', ['$event'])
onclick(event: MouseEvent): void {
// Note: this logic is taken from the VSCode handling of links in markdown
// Untrusted cells will not support commands or raw HTML tags
// Finally, we should consider supporting relative paths - created #5238 to track
let target: HTMLElement = event.target as HTMLElement;
if (target.tagName !== 'A') {
target = target.parentElement;
if (!target || target.tagName !== 'A') {
return;
}
}
try {
const href = target['href'];
if (href) {
this.handleLink(href);
}
} catch (err) {
onUnexpectedError(err);
} finally {
event.preventDefault();
}
}
private handleLink(content: string): void {
let uri: URI | undefined;
try {
uri = URI.parse(content);
} catch {
// ignore
}
if (uri && this.openerService && this.isSupportedLink(uri)) {
if (uri.fragment && uri.fragment.length > 0 && uri.path === this.workbenchFilePath.path) {
this.notebookService.navigateTo(this.notebookUri, uri.fragment);
} else {
this.openerService.open(uri).catch(onUnexpectedError);
}
}
}
private isSupportedLink(link: URI): boolean {
if (knownSchemes.has(link.scheme)) {
return true;
}
return !!this.isTrusted && link.scheme === 'command';
}
}

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>execute_cell_inverse </title><circle class="cls-1" cx="8" cy="7.92" r="7.76"/><polygon points="10.7 8 6.67 11 6.67 5 10.7 8 10.7 8"/></svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@@ -0,0 +1,16 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs><style>.cls-1{fill:#c1d6e6;}.cls-2{fill:#0078d4;}.cls-3{fill:#fff;}</style></defs>
<title>stop_cell_solidanimation_inverse</title>
<path class="cls-1" d="M8,16a8,8,0,1,1,8-8A8,8,0,0,1,8,16ZM8,1a7,7,0,1,0,7,7A7,7,0,0,0,8,1Z"/>
<path class="cls-2" d="M8.51,0v1A7,7,0,0,1,15,8a6.87,6.87,0,0,1-1.07,3.7l.81.64A7.92,7.92,0,0,0,16,8,8,8,0,0,0,8.51,0Z">
<animateTransform attributeName="transform"
type="rotate"
from="0 8 8"
to="360 8 8"
begin="0s"
dur="1.5s"
repeatCount="indefinite"
/>
</path>
<circle class="cls-3" cx="8" cy="8" r="6.32"/>
<rect x="4.91" y="4.91" width="6.18" height="6.18"/></svg>

After

Width:  |  Height:  |  Size: 774 B

View File

@@ -0,0 +1,191 @@
/*
https://raw.githubusercontent.com/isagalaev/highlight.js/master/src/styles/vs2015.css
*/
/*
* Visual Studio 2015 dark style
* Author: Nicolas LLOBERA <nllobera@gmail.com>
*/
.notebook-preview .hljs-keyword,
.notebook-preview .hljs-literal,
.notebook-preview .hljs-symbol,
.notebook-preview .hljs-name {
color: #569CD6;
}
.notebook-preview .hljs-link {
color: #569CD6;
text-decoration: underline;
}
.notebook-preview .hljs-built_in,
.notebook-preview .hljs-type {
color: #4EC9B0;
}
.notebook-preview .hljs-number,
.notebook-preview .hljs-class {
color: #B8D7A3;
}
.notebook-preview .hljs-string,
.notebook-preview .hljs-meta-string {
color: #D69D85;
}
.notebook-preview .hljs-regexp,
.notebook-preview .hljs-template-tag {
color: #9A5334;
}
.notebook-preview .hljs-subst,
.notebook-preview .hljs-function,
.notebook-preview .hljs-title,
.notebook-preview .hljs-params,
.notebook-preview .hljs-formula {
color: #DCDCDC;
}
.notebook-preview pre code .hljs-subst,
.notebook-preview pre code .hljs-function,
.notebook-preview pre code .hljs-title,
.notebook-preview pre code .hljs-params,
.notebook-preview pre code .hljs-formula {
color: var(--vscode-editor-foreground);
}
.notebook-preview .hljs-comment,
.notebook-preview .hljs-quote {
color: #57A64A;
font-style: italic;
}
.notebook-preview .hljs-doctag {
color: #608B4E;
}
.notebook-preview .hljs-meta,
.notebook-preview .hljs-meta-keyword,
.notebook-preview .hljs-tag {
color: #9B9B9B;
}
.notebook-preview .hljs-variable,
.notebook-preview .hljs-template-variable {
color: #BD63C5;
}
.notebook-preview .hljs-attr,
.notebook-preview .hljs-attribute,
.notebook-preview .hljs-builtin-name {
color: #9CDCFE;
}
.notebook-preview .hljs-section {
color: gold;
}
.notebook-preview .hljs-emphasis {
font-style: italic;
}
.notebook-preview .hljs-strong {
font-weight: bold;
}
/*.hljs-code {
font-family:'Monospace';
}*/
.notebook-preview .hljs-bullet,
.notebook-preview .hljs-selector-tag,
.notebook-preview .hljs-selector-id,
.notebook-preview .hljs-selector-class,
.notebook-preview .hljs-selector-attr,
.notebook-preview .hljs-selector-pseudo {
color: #D7BA7D;
}
.notebook-preview .hljs-addition {
background-color: var(--vscode-diffEditor-insertedTextBackground, rgba(155, 185, 85, 0.2));
color: rgb(155, 185, 85);
display: inline-block;
width: 100%;
}
.notebook-preview .hljs-deletion {
background: var(--vscode-diffEditor-removedTextBackground, rgba(255, 0, 0, 0.2));
color: rgb(255, 0, 0);
display: inline-block;
width: 100%;
}
/*
From https://raw.githubusercontent.com/isagalaev/highlight.js/master/src/styles/vs.css
*/
/*
Visual Studio-like style based on original C# coloring by Jason Diamond <jason@diamond.name>
*/
.notebook-preview .hljs-function,
.notebook-preview .hljs-params {
color: inherit;
}
.notebook-preview .hljs-comment,
.notebook-preview .hljs-quote,
.notebook-preview .hljs-variable {
color: #008000;
}
.notebook-preview .hljs-keyword,
.notebook-preview .hljs-selector-tag,
.notebook-preview .hljs-built_in,
.notebook-preview .hljs-name,
.notebook-preview .hljs-tag {
color: #00f;
}
.notebook-preview .hljs-string,
.notebook-preview .hljs-title,
.notebook-preview .hljs-section,
.notebook-preview .hljs-attribute,
.notebook-preview .hljs-literal,
.notebook-preview .hljs-template-tag,
.notebook-preview .hljs-template-variable,
.notebook-preview .hljs-type {
color: #a31515;
}
.notebook-preview .hljs-selector-attr,
.notebook-preview .hljs-selector-pseudo,
.notebook-preview .hljs-meta {
color: #2b91af;
}
.notebook-preview .hljs-doctag {
color: #808080;
}
.notebook-preview .hljs-attr {
color: #f00;
}
.notebook-preview .hljs-symbol,
.notebook-preview .hljs-bullet,
.notebook-preview .hljs-link {
color: #00b0e8;
}
.notebook-preview .hljs-emphasis {
font-style: italic;
}
.notebook-preview .hljs-strong {
font-weight: bold;
}

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>execute_cell</title><circle cx="8" cy="7.92" r="7.76"/><polygon class="cls-1" points="10.7 8 6.67 11 6.67 5 10.7 8 10.7 8"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#d02e00;}.cls-2{fill:#fff;}</style></defs><title>execute_cell_error</title><circle class="cls-1" cx="8" cy="7.92" r="7.76"/><polygon class="cls-2" points="10.7 8 6.67 11 6.67 5 10.7 8 10.7 8"/></svg>

After

Width:  |  Height:  |  Size: 317 B

View File

@@ -0,0 +1,16 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs><style>.cls-1{fill:#c1d6e6;}.cls-2{fill:#0078d4;}.cls-3{fill:#fff;}</style></defs>
<title>stop_cell_solidanimation</title>
<path class="cls-1" d="M8,16a8,8,0,1,1,8-8A8,8,0,0,1,8,16ZM8,1a7,7,0,1,0,7,7A7,7,0,0,0,8,1Z"/>
<path class="cls-2" d="M8.51,0v1A7,7,0,0,1,15,8a6.87,6.87,0,0,1-1.07,3.7l.81.64A7.92,7.92,0,0,0,16,8,8,8,0,0,0,8.51,0Z">
<animateTransform attributeName="transform"
type="rotate"
from="0 8 8"
to="360 8 8"
begin="0s"
dur="1.5s"
repeatCount="indefinite"
/>
</path>
<circle cx="8" cy="8" r="6.32"/>
<rect class="cls-3" x="4.91" y="4.91" width="6.18" height="6.18"/></svg>

After

Width:  |  Height:  |  Size: 766 B

View File

@@ -0,0 +1,231 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.notebook-preview {
font-size: 14px;
line-height: 22px;
word-wrap: break-word;
}
.notebook-preview #code-csp-warning {
position: fixed;
top: 0;
right: 0;
color: white;
margin: 16px;
text-align: center;
font-size: 12px;
font-family: sans-serif;
background-color:#444444;
cursor: pointer;
padding: 6px;
box-shadow: 1px 1px 1px rgba(0,0,0,.25);
}
.notebook-preview #code-csp-warning:hover {
text-decoration: none;
background-color:#007acc;
box-shadow: 2px 2px 2px rgba(0,0,0,.25);
}
.notebook-preview .scrollBeyondLastLine {
margin-bottom: calc(100vh - 22px);
}
.notebook-preview .showEditorSelection .code-line {
position: relative;
}
.notebook-preview .showEditorSelection .code-active-line:before,
.notebook-preview .showEditorSelection .code-line:hover:before {
content: "";
display: block;
position: absolute;
top: 0;
left: -12px;
height: 100%;
}
.notebook-preview .showEditorSelection li.code-active-line:before,
.notebook-preview .showEditorSelection li.code-line:hover:before {
left: -30px;
}
.notebook-preview .showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(0, 0, 0, 0.15);
}
.notebook-preview .showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(0, 0, 0, 0.40);
}
.notebook-preview .showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
.vs-dark .notebook-preview .showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(255, 255, 255, 0.4);
}
.vs-dark .notebook-preview .showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(255, 255, 255, 0.60);
}
.vs-dark .notebook-preview .showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
.hc-black .notebook-preview .showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(255, 160, 0, 0.7);
}
.hc-black .notebook-preview .showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(255, 160, 0, 1);
}
.hc-black .notebook-preview .showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
.notebook-preview img {
max-width: 100%;
max-height: 100%;
}
.notebookEditor a {
text-decoration: none;
}
.notebookEditor a:hover {
text-decoration: underline;
}
.notebook-preview a:focus,
.notebook-preview input:focus,
.notebook-preview select:focus,
.notebook-preview textarea:focus {
outline: 1px solid -webkit-focus-ring-color;
outline-offset: -1px;
}
.notebook-preview hr {
border: 0;
height: 2px;
border-bottom: 2px solid;
}
.notebook-preview h1 {
padding-bottom: 0.3em;
line-height: 1.2;
border-bottom-width: 1px;
border-bottom-style: solid;
}
.notebook-preview h1, .notebook-preview h2, .notebook-preview h3 {
font-weight: normal;
}
.notebook-preview h1 code,
.notebook-preview h2 code,
.notebook-preview h3 code,
.notebook-preview h4 code,
.notebook-preview h5 code,
.notebook-preview h6 code {
font-size: inherit;
line-height: auto;
}
.notebook-preview table {
border-collapse: collapse;
}
.notebook-preview table > thead > tr > th {
text-align: left;
border-bottom: 1px solid;
}
.notebook-preview table > thead > tr > th,
.notebook-preview table > thead > tr > td,
.notebook-preview table > tbody > tr > th,
.notebook-preview .notebook-preview table > tbody > tr > td {
padding: 5px 10px;
}
.notebook-preview table > tbody > tr + tr > td {
border-top: 1px solid;
}
.notebook-preview blockquote {
margin: 0 7px 0 5px;
padding: 0 16px 0 10px;
border-left-width: 5px;
border-left-style: solid;
}
.notebook-preview code {
font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback";
font-size: 12px;
line-height: 19px;
}
.notebook-preview pre {
white-space: pre-wrap;
}
.notebook-preview .mac code {
font-size: 12px;
line-height: 18px;
}
.notebook-preview pre:not(.hljs),
.notebook-preview pre.hljs code > div {
padding: 16px;
border-radius: 3px;
overflow: auto;
}
/** Theming */
.notebook-preview pre code {
color: var(--vscode-editor-foreground);
}
.notebook-preview pre {
background-color: rgba(220, 220, 220, 0.4);
}
.vs-dark .notebook-preview pre {
background-color: rgba(10, 10, 10, 0.4);
}
.hc-black .notebook-preview pre {
background-color: rgb(0, 0, 0);
}
.hc-black .notebook-preview h1 {
border-color: rgb(0, 0, 0);
}
.notebook-preview table > thead > tr > th {
border-color: rgba(0, 0, 0, 0.69);
}
.vs-dark .notebook-preview table > thead > tr > th {
border-color: rgba(255, 255, 255, 0.69);
}
.notebook-preview h1,
.notebook-preview hr,
.notebook-preview table > tbody > tr + tr > td {
border-color: rgba(0, 0, 0, 0.18);
}
.vs-dark .notebook-preview h1,
.vs-dark .notebook-preview hr,
.vs-dark .notebook-preview table > tbody > tr + tr > td {
border-color: rgba(255, 255, 255, 0.18);
}

View File

@@ -0,0 +1,470 @@
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
/*-----------------------------------------------------------------------------
| RenderedText
|----------------------------------------------------------------------------*/
output-component .jp-RenderedText {
text-align: left;
padding-left: var(--jp-code-padding);
font-size: var(--jp-code-font-size);
line-height: var(--jp-code-line-height);
font-family: var(--jp-code-font-family);
}
output-component .jp-RenderedText pre,
.jp-RenderedJavaScript pre,
output-component .jp-RenderedHTMLCommon pre {
color: var(--jp-content-font-color1);
border: none;
margin: 0px;
padding: 0px;
}
/* ansi_up creates classed spans for console foregrounds and backgrounds. */
output-component .jp-RenderedText pre .ansi-black-fg {
color: #3e424d;
}
output-component .jp-RenderedText pre .ansi-red-fg {
color: #e75c58;
}
output-component .jp-RenderedText pre .ansi-green-fg {
color: #00a250;
}
output-component .jp-RenderedText pre .ansi-yellow-fg {
color: #ddb62b;
}
output-component .jp-RenderedText pre .ansi-blue-fg {
color: #208ffb;
}
output-component .jp-RenderedText pre .ansi-magenta-fg {
color: #d160c4;
}
output-component .jp-RenderedText pre .ansi-cyan-fg {
color: #60c6c8;
}
output-component .jp-RenderedText pre .ansi-white-fg {
color: #c5c1b4;
}
output-component .jp-RenderedText pre .ansi-black-bg {
background-color: #3e424d;
}
output-component .jp-RenderedText pre .ansi-red-bg {
background-color: #e75c58;
}
output-component .jp-RenderedText pre .ansi-green-bg {
background-color: #00a250;
}
output-component .jp-RenderedText pre .ansi-yellow-bg {
background-color: #ddb62b;
}
output-component .jp-RenderedText pre .ansi-blue-bg {
background-color: #208ffb;
}
output-component .jp-RenderedText pre .ansi-magenta-bg {
background-color: #d160c4;
}
output-component .jp-RenderedText pre .ansi-cyan-bg {
background-color: #60c6c8;
}
output-component .jp-RenderedText pre .ansi-white-bg {
background-color: #c5c1b4;
}
output-component .jp-RenderedText pre .ansi-bright-black-fg {
color: #282c36;
}
output-component .jp-RenderedText pre .ansi-bright-red-fg {
color: #b22b31;
}
output-component .jp-RenderedText pre .ansi-bright-green-fg {
color: #007427;
}
output-component .jp-RenderedText pre .ansi-bright-yellow-fg {
color: #b27d12;
}
output-component .jp-RenderedText pre .ansi-bright-blue-fg {
color: #0065ca;
}
output-component .jp-RenderedText pre .ansi-bright-magenta-fg {
color: #a03196;
}
output-component .jp-RenderedText pre .ansi-bright-cyan-fg {
color: #258f8f;
}
output-component .jp-RenderedText pre .ansi-bright-white-fg {
color: #a1a6b2;
}
output-component .jp-RenderedText pre .ansi-bright-black-bg {
background-color: #282c36;
}
output-component .jp-RenderedText pre .ansi-bright-red-bg {
background-color: #b22b31;
}
output-component .jp-RenderedText pre .ansi-bright-green-bg {
background-color: #007427;
}
output-component .jp-RenderedText pre .ansi-bright-yellow-bg {
background-color: #b27d12;
}
output-component .jp-RenderedText pre .ansi-bright-blue-bg {
background-color: #0065ca;
}
output-component .jp-RenderedText pre .ansi-bright-magenta-bg {
background-color: #a03196;
}
output-component .jp-RenderedText pre .ansi-bright-cyan-bg {
background-color: #258f8f;
}
output-component .jp-RenderedText pre .ansi-bright-white-bg {
background-color: #a1a6b2;
}
output-component .jp-RenderedText[data-mime-type='application/vnd.jupyter.stderr'] {
background: var(--jp-rendermime-error-background);
padding-top: var(--jp-code-padding);
}
/*-----------------------------------------------------------------------------
| RenderedLatex
|----------------------------------------------------------------------------*/
.jp-RenderedLatex {
color: var(--jp-content-font-color1);
font-size: var(--jp-content-font-size1);
line-height: var(--jp-content-line-height);
}
/* Left-justify outputs.*/
.jp-OutputArea-output.jp-RenderedLatex {
text-align: left;
}
/*-----------------------------------------------------------------------------
| RenderedHTML
|----------------------------------------------------------------------------*/
output-component .jp-RenderedHTMLCommon {
color: var(--jp-content-font-color1);
font-family: var(--jp-content-font-family);
font-size: var(--jp-content-font-size1);
line-height: var(--jp-content-line-height);
/* Give a bit more R padding on Markdown text to keep line lengths reasonable */
padding-right: 20px;
}
output-component .jp-RenderedHTMLCommon em {
font-style: italic;
}
output-component .jp-RenderedHTMLCommon strong {
font-weight: bold;
}
output-component .jp-RenderedHTMLCommon u {
text-decoration: underline;
}
output-component .jp-RenderedHTMLCommon a:link {
text-decoration: none;
}
output-component .jp-RenderedHTMLCommon a:hover {
text-decoration: underline;
}
output-component .jp-RenderedHTMLCommon a:visited {
text-decoration: none;
}
/* Headings */
output-component .jp-RenderedHTMLCommon h1,
output-component .jp-RenderedHTMLCommon h2,
output-component .jp-RenderedHTMLCommon h3,
output-component .jp-RenderedHTMLCommon h4,
output-component .jp-RenderedHTMLCommon h5,
output-component .jp-RenderedHTMLCommon h6 {
line-height: var(--jp-content-heading-line-height);
font-weight: var(--jp-content-heading-font-weight);
font-style: normal;
margin: var(--jp-content-heading-margin-top) 0
var(--jp-content-heading-margin-bottom) 0;
}
output-component .jp-RenderedHTMLCommon h1:first-child,
output-component .jp-RenderedHTMLCommon h2:first-child,
output-component .jp-RenderedHTMLCommon h3:first-child,
output-component .jp-RenderedHTMLCommon h4:first-child,
output-component .jp-RenderedHTMLCommon h5:first-child,
output-component .jp-RenderedHTMLCommon h6:first-child {
margin-top: calc(0.5 * var(--jp-content-heading-margin-top));
}
output-component .jp-RenderedHTMLCommon h1:last-child,
output-component .jp-RenderedHTMLCommon h2:last-child,
output-component .jp-RenderedHTMLCommon h3:last-child,
output-component .jp-RenderedHTMLCommon h4:last-child,
output-component .jp-RenderedHTMLCommon h5:last-child,
output-component .jp-RenderedHTMLCommon h6:last-child {
margin-bottom: calc(0.5 * var(--jp-content-heading-margin-bottom));
}
output-component .jp-RenderedHTMLCommon h1 {
font-size: var(--jp-content-font-size5);
}
output-component .jp-RenderedHTMLCommon h2 {
font-size: var(--jp-content-font-size4);
}
output-component .jp-RenderedHTMLCommon h3 {
font-size: var(--jp-content-font-size3);
}
output-component .jp-RenderedHTMLCommon h4 {
font-size: var(--jp-content-font-size2);
}
output-component .jp-RenderedHTMLCommon h5 {
font-size: var(--jp-content-font-size1);
}
output-component .jp-RenderedHTMLCommon h6 {
font-size: var(--jp-content-font-size0);
}
/* Lists */
output-component .jp-RenderedHTMLCommon ul:not(.list-inline),
output-component .jp-RenderedHTMLCommon ol:not(.list-inline) {
padding-left: 2em;
}
output-component .jp-RenderedHTMLCommon ul {
list-style: disc;
}
output-component .jp-RenderedHTMLCommon ul ul {
list-style: square;
}
output-component .jp-RenderedHTMLCommon ul ul ul {
list-style: circle;
}
output-component .jp-RenderedHTMLCommon ol {
list-style: decimal;
}
output-component .jp-RenderedHTMLCommon ol ol {
list-style: upper-alpha;
}
output-component .jp-RenderedHTMLCommon ol ol ol {
list-style: lower-alpha;
}
output-component .jp-RenderedHTMLCommon ol ol ol ol {
list-style: lower-roman;
}
output-component .jp-RenderedHTMLCommon ol ol ol ol ol {
list-style: decimal;
}
output-component .jp-RenderedHTMLCommon ol,
output-component .jp-RenderedHTMLCommon ul {
margin-bottom: 1em;
}
output-component .jp-RenderedHTMLCommon ul ul,
output-component .jp-RenderedHTMLCommon ul ol,
output-component .jp-RenderedHTMLCommon ol ul,
output-component .jp-RenderedHTMLCommon ol ol {
margin-bottom: 0em;
}
output-component .jp-RenderedHTMLCommon hr {
color: var(--jp-border-color2);
background-color: var(--jp-border-color1);
margin-top: 1em;
margin-bottom: 1em;
}
output-component .jp-RenderedHTMLCommon > pre {
margin: 1.5em 2em;
}
output-component .jp-RenderedHTMLCommon pre,
output-component .jp-RenderedHTMLCommon code {
border: 0;
background-color: var(--jp-layout-color0);
color: var(--jp-content-font-color1);
font-family: var(--jp-code-font-family);
font-size: inherit;
line-height: var(--jp-code-line-height);
padding: 0;
}
output-component .jp-RenderedHTMLCommon p > code {
background-color: var(--jp-layout-color2);
padding: 1px 5px;
}
/* Tables */
output-component .jp-RenderedHTMLCommon table {
border-collapse: collapse;
border-spacing: 0;
border: none;
color: var(--jp-ui-font-color1);
font-size: 12px;
table-layout: auto;
margin-left: auto;
margin-right: auto;
}
output-component .jp-RenderedHTMLCommon thead {
border-bottom: var(--jp-border-width) solid var(--jp-border-color1);
vertical-align: bottom;
}
output-component .jp-RenderedHTMLCommon td,
output-component .jp-RenderedHTMLCommon th,
output-component .jp-RenderedHTMLCommon tr {
text-align: left;
vertical-align: middle;
padding: 0.5em 0.5em;
line-height: normal;
white-space: normal;
max-width: none;
border: none;
}
.jp-RenderedMarkdown.jp-RenderedHTMLCommon td,
.jp-RenderedMarkdown.jp-RenderedHTMLCommon th {
max-width: none;
}
output-component th {
font-weight: bold;
}
output-component .jp-RenderedHTMLCommon tbody tr:nth-child(odd) {
background: var(--jp-layout-color0);
}
output-component .jp-RenderedHTMLCommon tbody tr:nth-child(even) {
background: var(--jp-rendermime-table-row-background);
}
output-component .jp-RenderedHTMLCommon tbody tr:hover {
background: var(--jp-rendermime-table-row-hover-background);
}
output-component .jp-RenderedHTMLCommon table {
margin-bottom: 1em;
display: table-row;
}
output-component .jp-RenderedHTMLCommon p {
text-align: left;
margin: 0px;
}
output-component .jp-RenderedHTMLCommon p {
margin-bottom: 1em;
}
output-component .jp-RenderedHTMLCommon img {
-moz-force-broken-image-icon: 1;
}
/* Restrict to direct children as other images could be nested in other content. */
output-component .jp-RenderedHTMLCommon > img {
display: block;
margin-left: auto;
margin-right: auto;
margin-bottom: 1em;
}
/* Change color behind transparent images if they need it... */
[data-theme-light='false'] .jp-RenderedImage img.jp-needs-light-background {
background-color: var(--jp-inverse-layout-color1);
}
[data-theme-light='true'] .jp-RenderedImage img.jp-needs-dark-background {
background-color: var(--jp-inverse-layout-color1);
}
/* ...or leave it untouched if they don't */
[data-theme-light='false'] .jp-RenderedImage img.jp-needs-dark-background {
}
[data-theme-light='true'] .jp-RenderedImage img.jp-needs-light-background {
}
output-component .jp-RenderedHTMLCommon img,
.jp-RenderedImage img,
output-component .jp-RenderedHTMLCommon svg,
.jp-RenderedSVG svg {
max-width: 100%;
height: auto;
}
output-component .jp-RenderedHTMLCommon img.jp-mod-unconfined,
.jp-RenderedImage img.jp-mod-unconfined,
output-component .jp-RenderedHTMLCommon svg.jp-mod-unconfined,
.jp-RenderedSVG svg.jp-mod-unconfined {
max-width: none;
}
output-component .jp-RenderedHTMLCommon .alert {
margin-bottom: 1em;
}
output-component .jp-RenderedHTMLCommon blockquote {
margin: 1em 2em;
padding: 0 1em;
border-left: 5px solid var(--jp-border-color2);
}
a.jp-InternalAnchorLink {
visibility: hidden;
margin-left: 8px;
color: var(--md-blue-800);
}
h1:hover .jp-InternalAnchorLink,
h2:hover .jp-InternalAnchorLink,
h3:hover .jp-InternalAnchorLink,
h4:hover .jp-InternalAnchorLink,
h5:hover .jp-InternalAnchorLink,
h6:hover .jp-InternalAnchorLink {
visibility: visible;
}
/* Most direct children of .jp-RenderedHTMLCommon have a margin-bottom of 1.0.
* At the bottom of cells this is a bit too much as there is also spacing
* between cells. Going all the way to 0 gets too tight between markdown and
* code cells.
*/
output-component .jp-RenderedHTMLCommon > *:last-child {
margin-bottom: 0.5em;
}
/*-----------------------------------------------------------------------------
| RenderedPDF
|----------------------------------------------------------------------------*/
.jp-RenderedPDF {
font-size: var(--jp-ui-font-size1);
}
plotly-output .plotly-wrapper {
display: block;
overflow-y: hidden;
}

View File

@@ -0,0 +1,15 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div style="flex: 0 0 auto; user-select: none;">
<div #output class="output-userselect">
<ng-template component-host>
</ng-template>
<pre *ngIf="hasError" class="p-Widget jp-RenderedText">{{errorText}}</pre>
</div>
</div>
</div>

View File

@@ -0,0 +1,187 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./code';
import 'vs/css!./media/output';
import { OnInit, Component, Input, Inject, ElementRef, ViewChild, SimpleChange, AfterViewInit, forwardRef, ChangeDetectorRef, ComponentRef, ComponentFactoryResolver } from '@angular/core';
import { AngularDisposable } from 'sql/base/browser/lifecycle';
import { Event } from 'vs/base/common/event';
import { nb } from 'azdata';
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import * as outputProcessor from 'sql/workbench/parts/notebook/common/models/outputProcessor';
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
import * as DOM from 'vs/base/browser/dom';
import { ComponentHostDirective } from 'sql/workbench/parts/dashboard/browser/core/componentHost.directive';
import { Extensions, IMimeComponent, IMimeComponentRegistry } from 'sql/workbench/parts/notebook/browser/outputs/mimeRegistry';
import * as colors from 'vs/platform/theme/common/colorRegistry';
import * as themeColors from 'vs/workbench/common/theme';
import { Registry } from 'vs/platform/registry/common/platform';
import { localize } from 'vs/nls';
import * as types from 'vs/base/common/types';
import { getErrorMessage } from 'vs/base/common/errors';
export const OUTPUT_SELECTOR: string = 'output-component';
const USER_SELECT_CLASS = 'actionselect';
const componentRegistry = <IMimeComponentRegistry>Registry.as(Extensions.MimeComponentContribution);
@Component({
selector: OUTPUT_SELECTOR,
templateUrl: decodeURI(require.toUrl('./output.component.html'))
})
export class OutputComponent extends AngularDisposable implements OnInit, AfterViewInit {
@ViewChild('output', { read: ElementRef }) private outputElement: ElementRef;
@ViewChild(ComponentHostDirective) componentHost: ComponentHostDirective;
@Input() cellOutput: nb.ICellOutput;
@Input() cellModel: ICellModel;
private _trusted: boolean;
private _initialized: boolean = false;
private _activeCellId: string;
private _componentInstance: IMimeComponent;
public errorText: string;
constructor(
@Inject(IThemeService) private _themeService: IThemeService,
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeref: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) private _ref: ElementRef,
@Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver
) {
super();
}
ngOnInit() {
this._register(this._themeService.onThemeChange(event => this.updateTheme(event)));
this.loadComponent();
this.layout();
this._initialized = true;
this._register(Event.debounce(this.cellModel.notebookModel.layoutChanged, (l, e) => e, 50, /*leading=*/false)
(() => this.layout()));
}
ngAfterViewInit() {
this.updateTheme(this._themeService.getTheme());
}
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
for (let propName in changes) {
if (propName === 'activeCellId') {
this.toggleUserSelect(this.isActive());
break;
}
}
}
private toggleUserSelect(userSelect: boolean): void {
if (!this.nativeOutputElement) {
return;
}
if (userSelect) {
DOM.addClass(this.nativeOutputElement, USER_SELECT_CLASS);
} else {
DOM.removeClass(this.nativeOutputElement, USER_SELECT_CLASS);
}
}
private get nativeOutputElement() {
return this.outputElement ? this.outputElement.nativeElement : undefined;
}
public layout(): void {
if (this.componentInstance && this.componentInstance.layout) {
this.componentInstance.layout();
}
}
private get componentInstance(): IMimeComponent {
if (!this._componentInstance) {
this.loadComponent();
}
return this._componentInstance;
}
get trustedMode(): boolean {
return this._trusted;
}
@Input() set trustedMode(value: boolean) {
this._trusted = value;
if (this._initialized) {
this.layout();
}
}
@Input() set activeCellId(value: string) {
this._activeCellId = value;
}
get activeCellId(): string {
return this._activeCellId;
}
protected isActive() {
return this.cellModel && this.cellModel.id === this.activeCellId;
}
public hasError(): boolean {
return !types.isUndefinedOrNull(this.errorText);
}
private updateTheme(theme: ITheme): void {
let el = <HTMLElement>this._ref.nativeElement;
let backgroundColor = theme.getColor(colors.editorBackground, true);
let foregroundColor = theme.getColor(themeColors.SIDE_BAR_FOREGROUND, true);
if (backgroundColor) {
el.style.backgroundColor = backgroundColor.toString();
}
if (foregroundColor) {
el.style.color = foregroundColor.toString();
}
}
private loadComponent(): void {
let options = outputProcessor.getBundleOptions({ value: this.cellOutput, trusted: this.trustedMode });
options.themeService = this._themeService;
let mimeType = componentRegistry.getPreferredMimeType(
options.data,
options.trusted ? 'any' : 'ensure'
);
this.errorText = undefined;
if (!mimeType) {
this.errorText = localize('noMimeTypeFound', "No {0}renderer could be found for output. It has the following MIME types: {1}",
options.trusted ? '' : localize('safe', "safe "),
Object.keys(options.data).join(', '));
return;
}
let selector = componentRegistry.getCtorFromMimeType(mimeType);
if (!selector) {
this.errorText = localize('noSelectorFound', "No component could be found for selector {0}", mimeType);
return;
}
let componentFactory = this._componentFactoryResolver.resolveComponentFactory(selector);
let viewContainerRef = this.componentHost.viewContainerRef;
viewContainerRef.clear();
let componentRef: ComponentRef<IMimeComponent>;
try {
componentRef = viewContainerRef.createComponent(componentFactory, 0);
this._componentInstance = componentRef.instance;
this._componentInstance.mimeType = mimeType;
this._componentInstance.cellModel = this.cellModel;
this._componentInstance.bundleOptions = options;
this._changeref.detectChanges();
let el = <HTMLElement>componentRef.location.nativeElement;
// set widget styles to conform to its box
el.style.overflow = 'hidden';
el.style.position = 'relative';
} catch (e) {
this.errorText = localize('componentRenderError', "Error rendering component: {0}", getErrorMessage(e));
return;
}
}
}

View File

@@ -0,0 +1,12 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div #outputarea link-handler [isTrusted]="isTrusted" [notebookUri]="notebookUri" class="notebook-output" style="flex: 0 0 auto;">
<output-component *ngFor="let output of cellModel.outputs" [cellOutput]="output" [trustedMode] = "cellModel.trustedMode" [cellModel]="cellModel" [activeCellId]="activeCellId">
</output-component>
</div>
</div>

View File

@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./code';
import 'vs/css!./outputArea';
import { OnInit, Component, Input, Inject, ElementRef, ViewChild, forwardRef, ChangeDetectorRef } from '@angular/core';
import { AngularDisposable } from 'sql/base/browser/lifecycle';
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import * as themeColors from 'vs/workbench/common/theme';
import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { URI } from 'vs/base/common/uri';
export const OUTPUT_AREA_SELECTOR: string = 'output-area-component';
@Component({
selector: OUTPUT_AREA_SELECTOR,
templateUrl: decodeURI(require.toUrl('./outputArea.component.html'))
})
export class OutputAreaComponent extends AngularDisposable implements OnInit {
@ViewChild('outputarea', { read: ElementRef }) private outputArea: ElementRef;
@Input() cellModel: ICellModel;
private _activeCellId: string;
constructor(
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef
) {
super();
}
ngOnInit() {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
if (this.cellModel) {
this._register(this.cellModel.onOutputsChanged(e => {
if (!(this._changeRef['destroyed'])) {
this._changeRef.detectChanges();
if (e && e.shouldScroll) {
this.setFocusAndScroll(this.outputArea.nativeElement);
}
}
}));
}
}
public get isTrusted(): boolean {
return this.cellModel.trustedMode;
}
public get notebookUri(): URI {
return this.cellModel.notebookModel.notebookUri;
}
private setFocusAndScroll(node: HTMLElement): void {
if (node) {
node.focus();
node.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
@Input() set activeCellId(value: string) {
this._activeCellId = value;
}
get activeCellId(): string {
return this._activeCellId;
}
private updateTheme(theme: IColorTheme): void {
let outputElement = <HTMLElement>this.outputArea.nativeElement;
outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
}
}

View File

@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
output-area-component {
display: block;
}
output-area-component .notebook-output {
border-top-width: 0px;
user-select: text;
padding: 5px 20px 0px;
}
.output-userselect.actionselect {
user-select: text;
}
.output-userselect pre{
white-space: pre-wrap;
word-wrap: break-word;
}

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
placeholder-cell-component {
height: 50px;
width: 100%;
display: block;
box-shadow: 0px 4px 6px 0px rgba(0,0,0,0.14);
}
placeholder-cell-component .text {
display: flex;
align-items: center;
justify-content: center;
height: 50px;
-webkit-margin-before: 0em;
-webkit-margin-after: 0em;
}

View File

@@ -0,0 +1,13 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div class="placeholder-cell-component" style="flex: 0 0 auto;">
<div class="placeholder-cell-component text">
<p>{{clickOn}} <a href="#" (click)="addCell('code', $event)">{{plusCode}}</a> {{or}} <a href="#" (click)="addCell('markdown', $event)">{{plusText}}</a> {{toAddCell}}</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./placeholder';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, SimpleChange, OnChanges } from '@angular/core';
import { CellView } from 'sql/workbench/parts/notebook/browser/cellViews/interfaces';
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
import { localize } from 'vs/nls';
import { CellType } from 'sql/workbench/parts/notebook/common/models/contracts';
export const PLACEHOLDER_SELECTOR: string = 'placeholder-cell-component';
@Component({
selector: PLACEHOLDER_SELECTOR,
templateUrl: decodeURI(require.toUrl('./placeholderCell.component.html'))
})
export class PlaceholderCellComponent extends CellView implements OnInit, OnChanges {
@Input() cellModel: ICellModel;
@Input() set model(value: NotebookModel) {
this._model = value;
}
private _model: NotebookModel;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
) {
super();
}
ngOnInit() {
if (this.cellModel) {
this._register(this.cellModel.onOutputsChanged(() => {
this._changeRef.detectChanges();
}));
}
}
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
}
get model(): NotebookModel {
return this._model;
}
get clickOn(): string {
return localize('clickOn', "Click on");
}
get plusCode(): string {
return localize('plusCode', "+ Code");
}
get or(): string {
return localize('or', "or");
}
get plusText(): string {
return localize('plusText', "+ Text");
}
get toAddCell(): string {
return localize('toAddCell', "to add a code or text cell");
}
public addCell(cellType: string, event?: Event): void {
if (event) {
event.stopPropagation();
}
let type: CellType = <CellType>cellType;
if (!type) {
type = 'code';
}
this._model.addCell(<CellType>cellType);
}
public layout() {
}
}

View File

@@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./stdin';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef,
ViewChild, ElementRef, AfterViewInit, HostListener
} from '@angular/core';
import { nb } from 'azdata';
import { localize } from 'vs/nls';
import { IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { inputBackground, inputBorder } from 'vs/platform/theme/common/colorRegistry';
import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { attachInputBoxStyler } from 'sql/platform/theme/common/styler';
import { AngularDisposable } from 'sql/base/browser/lifecycle';
import { Deferred } from 'sql/base/common/promise';
import { ICellModel, CellExecutionState } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
export const STDIN_SELECTOR: string = 'stdin-component';
@Component({
selector: STDIN_SELECTOR,
template: `
<div class="prompt">{{prompt}}</div>
<div #input class="input"></div>
`
})
export class StdInComponent extends AngularDisposable implements AfterViewInit {
private _input: InputBox;
@ViewChild('input', { read: ElementRef }) private _inputContainer: ElementRef;
@Input() stdIn: nb.IStdinMessage;
@Input() onSendInput: Deferred<string>;
@Input() cellModel: ICellModel;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private changeRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IContextViewService) private contextViewService: IContextViewService,
@Inject(forwardRef(() => ElementRef)) private el: ElementRef
) {
super();
}
ngAfterViewInit(): void {
let inputOptions: IInputOptions = {
placeholder: '',
ariaLabel: this.prompt
};
this._input = new InputBox(this._inputContainer.nativeElement, this.contextViewService, inputOptions);
if (this.password) {
this._input.inputElement.type = 'password';
}
this._register(this._input);
this._register(attachInputBoxStyler(this._input, this.themeService, {
inputValidationInfoBackground: inputBackground,
inputValidationInfoBorder: inputBorder,
}));
if (this.cellModel) {
this._register(this.cellModel.onExecutionStateChange((status) => this.handleExecutionChange(status)));
}
this._input.focus();
}
@HostListener('document:keydown', ['$event'])
public handleKeyboardInput(event: KeyboardEvent): void {
let e = new StandardKeyboardEvent(event);
switch (e.keyCode) {
case KeyCode.Enter:
// Indi
if (this.onSendInput) {
this.onSendInput.resolve(this._input.value);
}
e.stopPropagation();
break;
case KeyCode.Escape:
if (this.onSendInput) {
this.onSendInput.reject('');
}
e.stopPropagation();
break;
default:
// No-op
break;
}
}
handleExecutionChange(status: CellExecutionState): void {
if (status !== CellExecutionState.Running && this.onSendInput) {
this.onSendInput.reject('');
}
}
private get prompt(): string {
if (this.stdIn && this.stdIn.content && this.stdIn.content.prompt) {
return this.stdIn.content.prompt;
}
return localize('stdInLabel', "StdIn:");
}
private get password(): boolean {
return this.stdIn && this.stdIn.content && this.stdIn.content.password;
}
}

View File

@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
stdin-component {
display: flex;
flex-flow: row;
padding: 10px;
align-items: center;
}
stdin-component .prompt {
flex: 0 0 auto;
}
stdin-component .input {
flex: 1 1 auto;
padding-left: 10px;
}

View File

@@ -0,0 +1,18 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column" (mouseover)="hover=true" (mouseleave)="hover=false">
<div class="notebook-text" style="flex: 0 0 auto;">
<code-component *ngIf="isEditMode" [cellModel]="cellModel" (onContentChanged)="handleContentChanged()" [model]="model" [activeCellId]="activeCellId">
</code-component>
</div>
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: row">
<div #preview link-handler [isTrusted]="isTrusted" [notebookUri]="notebookUri" class="notebook-preview" style="flex: 1 1 auto" (dblclick)="toggleEditMode()">
</div>
<div #moreactions class="moreActions" style="flex: 0 0 auto; display: flex; flex-flow:column;width: 20px; min-height: 20px; max-height: 20px; padding-top: 0px; orientation: portrait">
</div>
</div>
</div>

View File

@@ -0,0 +1,265 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./textCell';
import 'vs/css!./media/markdown';
import 'vs/css!./media/highlight';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnChanges, SimpleChange, HostListener } from '@angular/core';
import * as path from 'path';
import { localize } from 'vs/nls';
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import * as themeColors from 'vs/workbench/common/theme';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import * as DOM from 'vs/base/browser/dom';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { toDisposable } from 'vs/base/common/lifecycle';
import { IMarkdownRenderResult } from 'vs/editor/contrib/markdown/markdownRenderer';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { NotebookMarkdownRenderer } from 'sql/workbench/parts/notebook/browser/outputs/notebookMarkdown';
import { CellView } from 'sql/workbench/parts/notebook/browser/cellViews/interfaces';
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
import { ISanitizer, defaultSanitizer } from 'sql/workbench/parts/notebook/browser/outputs/sanitizer';
import { CellToggleMoreActions } from 'sql/workbench/parts/notebook/browser/cellToggleMoreActions';
import { CommonServiceInterface } from 'sql/platform/bootstrap/browser/commonServiceInterface.service';
import { useInProcMarkdown, convertVscodeResourceToFileInSubDirectories } from 'sql/workbench/parts/notebook/common/models/notebookUtils';
export const TEXT_SELECTOR: string = 'text-cell-component';
const USER_SELECT_CLASS = 'actionselect';
@Component({
selector: TEXT_SELECTOR,
templateUrl: decodeURI(require.toUrl('./textCell.component.html'))
})
export class TextCellComponent extends CellView implements OnInit, OnChanges {
@ViewChild('preview', { read: ElementRef }) private output: ElementRef;
@ViewChild('moreactions', { read: ElementRef }) private moreActionsElementRef: ElementRef;
@Input() cellModel: ICellModel;
@Input() set model(value: NotebookModel) {
this._model = value;
}
@Input() set activeCellId(value: string) {
this._activeCellId = value;
}
@Input() set hover(value: boolean) {
this._hover = value;
if (!this.isActive()) {
// Only make a change if we're not active, since this has priority
this.updateMoreActions();
}
}
@HostListener('document:keydown.escape', ['$event'])
handleKeyboardEvent() {
if (this.isEditMode) {
this.toggleEditMode(false);
}
this.cellModel.active = false;
this._model.activeCell = undefined;
}
private _content: string | string[];
private _lastTrustedMode: boolean;
private isEditMode: boolean;
private _sanitizer: ISanitizer;
private _model: NotebookModel;
private _activeCellId: string;
private readonly _onDidClickLink = this._register(new Emitter<URI>());
public readonly onDidClickLink = this._onDidClickLink.event;
private _cellToggleMoreActions: CellToggleMoreActions;
private _hover: boolean;
private markdownRenderer: NotebookMarkdownRenderer;
private markdownResult: IMarkdownRenderResult;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(ICommandService) private _commandService: ICommandService,
@Inject(IOpenerService) private readonly openerService: IOpenerService,
@Inject(IConfigurationService) private configurationService: IConfigurationService,
) {
super();
this.isEditMode = true;
this._cellToggleMoreActions = this._instantiationService.createInstance(CellToggleMoreActions);
this.markdownRenderer = this._instantiationService.createInstance(NotebookMarkdownRenderer);
this._register(toDisposable(() => {
if (this.markdownResult) {
this.markdownResult.dispose();
}
}));
}
//Gets sanitizer from ISanitizer interface
private get sanitizer(): ISanitizer {
if (this._sanitizer) {
return this._sanitizer;
}
return this._sanitizer = defaultSanitizer;
}
get model(): NotebookModel {
return this._model;
}
get activeCellId(): string {
return this._activeCellId;
}
private setLoading(isLoading: boolean): void {
this.cellModel.loaded = !isLoading;
this._changeRef.detectChanges();
}
ngOnInit() {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
this._cellToggleMoreActions.onInit(this.moreActionsElementRef, this.model, this.cellModel);
this.setFocusAndScroll();
this._register(this.cellModel.onOutputsChanged(e => {
this.updatePreview();
}));
}
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
for (let propName in changes) {
if (propName === 'activeCellId') {
let changedProp = changes[propName];
this._activeCellId = changedProp.currentValue;
this.toggleUserSelect(this.isActive());
// If the activeCellId is undefined (i.e. in an active cell update), don't unnecessarily set editMode to false;
// it will be set to true in a subsequent call to toggleEditMode()
if (changedProp.previousValue !== undefined) {
this.toggleEditMode(false);
}
break;
}
}
}
public get isTrusted(): boolean {
return this.model.trustedMode;
}
public get notebookUri(): URI {
return this.model.notebookUri;
}
/**
* Updates the preview of markdown component with latest changes
* If content is empty and in non-edit mode, default it to 'Double-click to edit'
* Sanitizes the data to be shown in markdown cell
*/
private updatePreview(): void {
let trustedChanged = this.cellModel && this._lastTrustedMode !== this.cellModel.trustedMode;
let contentChanged = this._content !== this.cellModel.source || this.cellModel.source.length === 0;
if (trustedChanged || contentChanged) {
this._lastTrustedMode = this.cellModel.trustedMode;
if (!this.cellModel.source && !this.isEditMode) {
this._content = localize('doubleClickEdit', "Double-click to edit");
} else {
this._content = this.cellModel.source;
}
if (useInProcMarkdown(this.configurationService)) {
this.markdownRenderer.setNotebookURI(this.cellModel.notebookModel.notebookUri);
this.markdownResult = this.markdownRenderer.render({
isTrusted: true,
value: Array.isArray(this._content) ? this._content.join('') : this._content
});
this.markdownResult.element.innerHTML = this.sanitizeContent(this.markdownResult.element.innerHTML);
this.setLoading(false);
let outputElement = <HTMLElement>this.output.nativeElement;
outputElement.innerHTML = this.markdownResult.element.innerHTML;
} else {
this._commandService.executeCommand<string>('notebook.showPreview', this.cellModel.notebookModel.notebookUri, this._content).then((htmlcontent) => {
htmlcontent = convertVscodeResourceToFileInSubDirectories(htmlcontent, this.cellModel);
htmlcontent = this.sanitizeContent(htmlcontent);
let outputElement = <HTMLElement>this.output.nativeElement;
outputElement.innerHTML = htmlcontent;
this.setLoading(false);
});
}
}
}
//Sanitizes the content based on trusted mode of Cell Model
private sanitizeContent(content: string): string {
if (this.cellModel && !this.cellModel.trustedMode) {
content = this.sanitizer.sanitize(content);
}
return content;
}
// Todo: implement layout
public layout() {
}
private updateTheme(theme: IColorTheme): void {
let outputElement = <HTMLElement>this.output.nativeElement;
outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
let moreActionsEl = <HTMLElement>this.moreActionsElementRef.nativeElement;
moreActionsEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
}
public handleContentChanged(): void {
this.updatePreview();
}
public toggleEditMode(editMode?: boolean): void {
this.isEditMode = editMode !== undefined ? editMode : !this.isEditMode;
this.updateMoreActions();
this.updatePreview();
this._changeRef.detectChanges();
}
private updateMoreActions(): void {
if (!this.isEditMode && (this.isActive() || this._hover)) {
this.toggleMoreActionsButton(true);
}
else {
this.toggleMoreActionsButton(false);
}
}
private toggleUserSelect(userSelect: boolean): void {
if (!this.output) {
return;
}
if (userSelect) {
DOM.addClass(this.output.nativeElement, USER_SELECT_CLASS);
} else {
DOM.removeClass(this.output.nativeElement, USER_SELECT_CLASS);
}
}
private setFocusAndScroll(): void {
this.toggleEditMode(this.isActive());
if (this.output && this.output.nativeElement) {
(<HTMLElement>this.output.nativeElement).scrollTo({ behavior: 'smooth' });
}
}
protected isActive() {
return this.cellModel && this.cellModel.id === this.activeCellId;
}
protected toggleMoreActionsButton(isActiveOrHovered: boolean) {
this._cellToggleMoreActions.toggleVisible(!isActiveOrHovered);
}
}

View File

@@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
text-cell-component {
display: block;
}
text-cell-component .notebook-preview {
user-select: none;
padding-left: 8px;
padding-right: 8px;
}
.notebook-preview.actionselect {
user-select: text;
}
text-cell-component table {
border-collapse: collapse;
border-spacing: 0;
border: none;
font-size: 12px;
table-layout: auto;
margin-left: auto;
margin-right: auto;
margin-bottom: 1em;
display: table-row;
}
text-cell-component thead {
vertical-align: bottom;
}
text-cell-component td,
text-cell-component th,
text-cell-component tr {
text-align: left;
vertical-align: middle;
padding: 0.5em 0.5em;
line-height: normal;
white-space: normal;
max-width: none;
border: none;
}
text-cell-component th {
font-weight: bold;
}

View File

@@ -0,0 +1,25 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div #toolbar class="editor-toolbar actionbar-container" style="flex: 0 0 auto; display: flex; flex-flow: row; width: 100%; align-items: center;">
</div>
<div #container class="scrollable" style="flex: 1 1 auto; position: relative; outline: none" (click)="unselectActiveCell()" (scroll)="scrollHandler($event)">
<loading-spinner [loading]="isLoading"></loading-spinner>
<div class="notebook-cell" *ngFor="let cell of cells" (click)="selectCell(cell, $event)" [class.active]="cell.active">
<code-cell-component *ngIf="cell.cellType === 'code'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId">
</code-cell-component>
<text-cell-component *ngIf="cell.cellType === 'markdown'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId">
</text-cell-component>
</div>
<div class="notebook-cell" *ngIf="(!cells || !cells.length) && !isLoading">
<placeholder-cell-component [cellModel]="cell" [model]="model">
</placeholder-cell-component>
</div>
<div class="book-nav" #bookNav [style.visibility]="navigationVisibility">
</div>
</div>
</div>

View File

@@ -0,0 +1,695 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { nb } from 'azdata';
import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnDestroy } from '@angular/core';
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import * as themeColors from 'vs/workbench/common/theme';
import { INotificationService, INotification, Severity } from 'vs/platform/notification/common/notification';
import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
import { MenuId, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions';
import { IAction, Action, IActionViewItem } from 'vs/base/common/actions';
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import * as DOM from 'vs/base/browser/dom';
import { AngularDisposable } from 'sql/base/browser/lifecycle';
import { CellTypes, CellType } from 'sql/workbench/parts/notebook/common/models/contracts';
import { ICellModel, IModelFactory, INotebookModel, NotebookContentChange } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { INotebookService, INotebookParams, INotebookManager, INotebookEditor, DEFAULT_NOTEBOOK_PROVIDER, SQL_NOTEBOOK_PROVIDER, INotebookSection, INavigationProvider } from 'sql/workbench/services/notebook/common/notebookService';
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
import { ModelFactory } from 'sql/workbench/parts/notebook/common/models/modelFactory';
import * as notebookUtils from 'sql/workbench/parts/notebook/common/models/notebookUtils';
import { Deferred } from 'sql/base/common/promise';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { KernelsDropdown, AttachToDropdown, AddCellAction, TrustedAction, RunAllCellsAction, ClearAllOutputsAction } from 'sql/workbench/parts/notebook/browser/notebookActions';
import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService';
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { CellMagicMapper } from 'sql/workbench/parts/notebook/common/models/cellMagicMapper';
import { IExtensionsViewlet, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions';
import { CellModel } from 'sql/workbench/parts/notebook/common/models/cell';
import { FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { isValidBasename } from 'vs/base/common/extpath';
import { basename } from 'vs/base/common/resources';
import { createErrorWithActions } from 'vs/base/common/errorsWithActions';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { ILogService } from 'vs/platform/log/common/log';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { LabeledMenuItemActionItem, fillInActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { Button } from 'sql/base/browser/ui/button/button';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { IBootstrapParams } from 'sql/platform/bootstrap/common/bootstrapParams';
import { getErrorMessage } from 'vs/base/common/errors';
import product from 'vs/platform/product/node/product';
export const NOTEBOOK_SELECTOR: string = 'notebook-component';
@Component({
selector: NOTEBOOK_SELECTOR,
templateUrl: decodeURI(require.toUrl('./notebook.component.html'))
})
export class NotebookComponent extends AngularDisposable implements OnInit, OnDestroy, INotebookEditor {
@ViewChild('toolbar', { read: ElementRef }) private toolbar: ElementRef;
@ViewChild('container', { read: ElementRef }) private container: ElementRef;
@ViewChild('bookNav', { read: ElementRef }) private bookNav: ElementRef;
private _model: NotebookModel;
private _isInErrorState: boolean = false;
private _errorMessage: string;
protected _actionBar: Taskbar;
protected isLoading: boolean;
private notebookManagers: INotebookManager[] = [];
private _modelReadyDeferred = new Deferred<NotebookModel>();
private profile: IConnectionProfile;
private _trustedAction: TrustedAction;
private _runAllCellsAction: RunAllCellsAction;
private _providerRelatedActions: IAction[] = [];
private _scrollTop: number;
private _navProvider: INavigationProvider;
private navigationResult: nb.NavigationResult;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IConnectionManagementService) private connectionManagementService: IConnectionManagementService,
@Inject(IObjectExplorerService) private objectExplorerService: IObjectExplorerService,
@Inject(IEditorService) private editorService: IEditorService,
@Inject(INotificationService) private notificationService: INotificationService,
@Inject(INotebookService) private notebookService: INotebookService,
@Inject(IBootstrapParams) private _notebookParams: INotebookParams,
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
@Inject(IContextMenuService) private contextMenuService: IContextMenuService,
@Inject(IContextViewService) private contextViewService: IContextViewService,
@Inject(IConnectionDialogService) private connectionDialogService: IConnectionDialogService,
@Inject(IContextKeyService) private contextKeyService: IContextKeyService,
@Inject(IMenuService) private menuService: IMenuService,
@Inject(IKeybindingService) private keybindingService: IKeybindingService,
@Inject(IViewletService) private viewletService: IViewletService,
@Inject(ICapabilitiesService) private capabilitiesService: ICapabilitiesService,
@Inject(ITextFileService) private textFileService: ITextFileService,
@Inject(ILogService) private readonly logService: ILogService,
@Inject(ITelemetryService) private telemetryService: ITelemetryService
) {
super();
this.updateProfile();
this.isLoading = true;
}
private updateProfile(): void {
this.profile = this.notebookParams ? this.notebookParams.profile : undefined;
if (!this.profile) {
// Second use global connection if possible
let profile: IConnectionProfile = TaskUtilities.getCurrentGlobalConnection(this.objectExplorerService, this.connectionManagementService, this.editorService);
// TODO use generic method to match kernel with valid connection that's compatible. For now, we only have 1
if (profile && profile.providerName) {
this.profile = profile;
} else {
// if not, try 1st active connection that matches our filter
let activeProfiles = this.connectionManagementService.getActiveConnections();
if (activeProfiles && activeProfiles.length > 0) {
this.profile = activeProfiles[0];
}
}
}
}
ngOnInit() {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
this.initActionBar();
this.setScrollPosition();
this.doLoad();
this.initNavSection();
}
ngOnDestroy() {
this.dispose();
if (this.notebookService) {
this.notebookService.removeNotebookEditor(this);
}
}
public get model(): NotebookModel | null {
return this._model;
}
public get activeCellId(): string {
return this._model && this._model.activeCell ? this._model.activeCell.id : '';
}
public get cells(): ICellModel[] {
return this._model ? this._model.cells : [];
}
private updateTheme(theme: IColorTheme): void {
let toolbarEl = <HTMLElement>this.toolbar.nativeElement;
toolbarEl.style.borderBottomColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
}
public selectCell(cell: ICellModel, event?: Event) {
if (event) {
event.stopPropagation();
}
if (cell !== this.model.activeCell) {
if (this.model.activeCell) {
this.model.activeCell.active = false;
}
this._model.activeCell = cell;
this._model.activeCell.active = true;
this.detectChanges();
}
}
//Saves scrollTop value on scroll change
public scrollHandler(event: Event) {
this._scrollTop = (<HTMLElement>event.srcElement).scrollTop;
}
public unselectActiveCell() {
if (this.model && this.model.activeCell) {
this.model.activeCell.active = false;
this.model.activeCell = undefined;
}
this.detectChanges();
}
// Add cell based on cell type
public addCell(cellType: CellType) {
this._model.addCell(cellType);
}
public onKeyDown(event) {
switch (event.key) {
case 'ArrowDown':
case 'ArrowRight':
let nextIndex = (this.findCellIndex(this.model.activeCell) + 1) % this.cells.length;
this.selectCell(this.cells[nextIndex]);
break;
case 'ArrowUp':
case 'ArrowLeft':
let index = this.findCellIndex(this.model.activeCell);
if (index === 0) {
index = this.cells.length;
}
this.selectCell(this.cells[--index]);
break;
default:
break;
}
}
private setScrollPosition(): void {
if (this._notebookParams && this._notebookParams.input) {
this._notebookParams.input.layoutChanged(() => {
let containerElement = <HTMLElement>this.container.nativeElement;
containerElement.scrollTop = this._scrollTop;
});
}
}
private async doLoad(): Promise<void> {
try {
await this.createModelAndLoadContents();
await this.setNotebookManager();
await this.loadModel();
this._modelReadyDeferred.resolve(this._model);
this.notebookService.addNotebookEditor(this);
} catch (error) {
if (error) {
// Offer to create a file from the error if we have a file not found and the name is valid
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND && isValidBasename(basename(this.notebookParams.notebookUri))) {
let errorWithAction = createErrorWithActions(toErrorMessage(error), {
actions: [
new Action('workbench.files.action.createMissingFile', localize('createFile', "Create File"), undefined, true, () => {
return this.textFileService.create(this.notebookParams.notebookUri).then(() => this.editorService.openEditor({
resource: this.notebookParams.notebookUri,
options: {
pinned: true // new file gets pinned by default
}
}));
})
]
});
this.notificationService.error(errorWithAction);
let editors = this.editorService.visibleControls;
for (let editor of editors) {
if (editor && editor.input.getResource() === this._notebookParams.input.notebookUri) {
await editor.group.closeEditor(this._notebookParams.input, { preserveFocus: true });
break;
}
}
} else {
this.setViewInErrorState(localize('displayFailed', "Could not display contents: {0}", getErrorMessage(error)));
this.setLoading(false);
this._modelReadyDeferred.reject(error);
this.notebookService.addNotebookEditor(this);
}
}
}
}
private setLoading(isLoading: boolean): void {
this.isLoading = isLoading;
this.detectChanges();
}
private async loadModel(): Promise<void> {
// Wait on provider information to be available before loading kernel and other information
await this.awaitNonDefaultProvider();
await this._model.requestModelLoad();
this.detectChanges();
await this._model.startSession(this._model.notebookManager, undefined, true);
this.setContextKeyServiceWithProviderId(this._model.providerId);
this.fillInActionsForCurrentContext();
this.detectChanges();
}
private async createModelAndLoadContents(): Promise<void> {
let model = new NotebookModel({
factory: this.modelFactory,
notebookUri: this._notebookParams.notebookUri,
connectionService: this.connectionManagementService,
notificationService: this.notificationService,
notebookManagers: this.notebookManagers,
contentManager: this._notebookParams.input.contentManager,
cellMagicMapper: new CellMagicMapper(this.notebookService.languageMagics),
providerId: 'sql',
defaultKernel: this._notebookParams.input.defaultKernel,
layoutChanged: this._notebookParams.input.layoutChanged,
capabilitiesService: this.capabilitiesService,
editorLoadedTimestamp: this._notebookParams.input.editorOpenedTimestamp
}, this.profile, this.logService, this.notificationService, this.telemetryService);
let trusted = await this.notebookService.isNotebookTrustCached(this._notebookParams.notebookUri, this.isDirty());
model.onError((errInfo: INotification) => this.handleModelError(errInfo));
model.contentChanged((change) => this.handleContentChanged(change));
model.onProviderIdChange((provider) => this.handleProviderIdChanged(provider));
model.kernelChanged((kernelArgs) => this.handleKernelChanged(kernelArgs));
this._model = this._register(model);
await this._model.loadContents(trusted);
this.setLoading(false);
this.updateToolbarComponents(this._model.trustedMode);
this.detectChanges();
}
private async setNotebookManager(): Promise<void> {
let providerInfo = await this._notebookParams.providerInfo;
for (let providerId of providerInfo.providers) {
let notebookManager = await this.notebookService.getOrCreateNotebookManager(providerId, this._notebookParams.notebookUri);
this.notebookManagers.push(notebookManager);
}
}
private async awaitNonDefaultProvider(): Promise<void> {
// Wait on registration for now. Long-term would be good to cache and refresh
await this.notebookService.registrationComplete;
this.model.standardKernels = this._notebookParams.input.standardKernels;
// Refresh the provider if we had been using default
let providerInfo = await this._notebookParams.providerInfo;
if (DEFAULT_NOTEBOOK_PROVIDER === providerInfo.providerId) {
let providers = notebookUtils.getProvidersForFileName(this._notebookParams.notebookUri.fsPath, this.notebookService);
let tsqlProvider = providers.find(provider => provider === SQL_NOTEBOOK_PROVIDER);
providerInfo.providerId = tsqlProvider ? SQL_NOTEBOOK_PROVIDER : providers[0];
}
if (DEFAULT_NOTEBOOK_PROVIDER === providerInfo.providerId) {
// If it's still the default, warn them they should install an extension
this.notificationService.prompt(Severity.Warning,
localize('noKernelInstalled', "Please install the SQL Server 2019 extension to run cells."),
[{
label: localize('installSql2019Extension', "Install Extension"),
run: () => this.openExtensionGallery()
}]);
}
}
private async openExtensionGallery(): Promise<void> {
try {
let viewlet = await this.viewletService.openViewlet(VIEWLET_ID, true) as IExtensionsViewlet;
viewlet.search('sql-vnext');
viewlet.focus();
} catch (error) {
this.notificationService.error(error.message);
}
}
// Updates toolbar components
private updateToolbarComponents(isTrusted: boolean) {
if (this._trustedAction) {
this._trustedAction.enabled = true;
this._trustedAction.trusted = isTrusted;
}
}
private get modelFactory(): IModelFactory {
if (!this._notebookParams.modelFactory) {
this._notebookParams.modelFactory = new ModelFactory(this.instantiationService);
}
return this._notebookParams.modelFactory;
}
private handleModelError(notification: INotification): void {
this.notificationService.notify(notification);
}
private handleContentChanged(change: NotebookContentChange) {
// Note: for now we just need to set dirty state and refresh the UI.
this.detectChanges();
}
private handleProviderIdChanged(providerId: string) {
// If there are any actions that were related to the previous provider,
// disable them in the actionBar
this._providerRelatedActions.forEach(action => {
action.enabled = false;
});
this.setContextKeyServiceWithProviderId(providerId);
this.fillInActionsForCurrentContext();
}
private handleKernelChanged(kernelArgs: nb.IKernelChangedArgs) {
this.fillInActionsForCurrentContext();
}
findCellIndex(cellModel: ICellModel): number {
return this._model.cells.findIndex((cell) => cell.id === cellModel.id);
}
private setViewInErrorState(error: any): any {
this._isInErrorState = true;
this._errorMessage = getErrorMessage(error);
// For now, send message as error notification #870 covers having dedicated area for this
this.notificationService.error(error);
}
protected initActionBar(): void {
let kernelContainer = document.createElement('div');
let kernelDropdown = new KernelsDropdown(kernelContainer, this.contextViewService, this.modelReady);
kernelDropdown.render(kernelContainer);
attachSelectBoxStyler(kernelDropdown, this.themeService);
let attachToContainer = document.createElement('div');
let attachToDropdown = new AttachToDropdown(attachToContainer, this.contextViewService, this.modelReady,
this.connectionManagementService, this.connectionDialogService, this.notificationService, this.capabilitiesService, this.logService);
attachToDropdown.render(attachToContainer);
attachSelectBoxStyler(attachToDropdown, this.themeService);
let addCodeCellButton = new AddCellAction('notebook.AddCodeCell', localize('code', "Code"), 'notebook-button icon-add');
addCodeCellButton.cellType = CellTypes.Code;
let addTextCellButton = new AddCellAction('notebook.AddTextCell', localize('text', "Text"), 'notebook-button icon-add');
addTextCellButton.cellType = CellTypes.Markdown;
this._runAllCellsAction = this.instantiationService.createInstance(RunAllCellsAction, 'notebook.runAllCells', localize('runAll', "Run Cells"), 'notebook-button icon-run-cells');
let clearResultsButton = new ClearAllOutputsAction('notebook.ClearAllOutputs', localize('clearResults', "Clear Results"), 'notebook-button icon-clear-results');
this._trustedAction = this.instantiationService.createInstance(TrustedAction, 'notebook.Trusted');
this._trustedAction.enabled = false;
let taskbar = <HTMLElement>this.toolbar.nativeElement;
this._actionBar = new Taskbar(taskbar, { actionViewItemProvider: action => this.actionItemProvider(action as Action) });
this._actionBar.context = this;
this._actionBar.setContent([
{ action: addCodeCellButton },
{ action: addTextCellButton },
{ element: kernelContainer },
{ element: attachToContainer },
{ action: this._trustedAction },
{ action: this._runAllCellsAction },
{ action: clearResultsButton }
]);
}
protected initNavSection(): void {
this._navProvider = this.notebookService.getNavigationProvider(this._notebookParams.notebookUri);
if (product.quality !== 'stable' &&
this.contextKeyService.getContextKeyValue('bookOpened') &&
this._navProvider) {
this._navProvider.getNavigation(this._notebookParams.notebookUri).then(result => {
this.navigationResult = result;
this.addButton(localize('previousButtonLabel', "< Previous"),
() => this.previousPage(), this.navigationResult.previous ? true : false);
this.addButton(localize('nextButtonLabel', "Next >"),
() => this.nextPage(), this.navigationResult.next ? true : false);
this.detectChanges();
}, err => {
console.log(err);
});
}
}
public get navigationVisibility(): 'hidden' | 'visible' {
if (this.navigationResult) {
return this.navigationResult.hasNavigation ? 'visible' : 'hidden';
}
return 'hidden';
}
private addButton(label: string, onDidClick?: () => void, enabled?: boolean): void {
const container = DOM.append(this.bookNav.nativeElement, DOM.$('.dialog-message-button'));
let button = new Button(container);
button.icon = '';
button.label = label;
if (onDidClick) {
this._register(button.onDidClick(onDidClick));
}
if (!enabled) {
button.enabled = false;
}
}
private actionItemProvider(action: Action): IActionViewItem {
// Check extensions to create ActionItem; otherwise, return undefined
// This is similar behavior that exists in MenuItemActionItem
if (action instanceof MenuItemAction) {
return new LabeledMenuItemActionItem(action, this.keybindingService, this.contextMenuService, this.notificationService, 'notebook-button');
}
return undefined;
}
/**
* Get all of the menu contributions that use the ID 'notebook/toolbar'.
* Then, find all groups (currently we don't leverage the contributed
* groups functionality for the notebook toolbar), and fill in the 'primary'
* array with items that don't list a group. Finally, add any actions from
* the primary array to the end of the toolbar.
*/
private fillInActionsForCurrentContext(): void {
let primary: IAction[] = [];
let secondary: IAction[] = [];
let notebookBarMenu = this.menuService.createMenu(MenuId.NotebookToolbar, this.contextKeyService);
let groups = notebookBarMenu.getActions({ arg: null, shouldForwardArgs: true });
fillInActions(groups, { primary, secondary }, false, (group: string) => group === undefined || group === '');
this.addPrimaryContributedActions(primary);
}
private detectChanges(): void {
if (!(this._changeRef['destroyed'])) {
this._changeRef.detectChanges();
}
}
private addPrimaryContributedActions(primary: IAction[]) {
for (let action of primary) {
// Need to ensure that we don't add the same action multiple times
let foundIndex = this._providerRelatedActions.findIndex(act => act.id === action.id);
if (foundIndex < 0) {
this._actionBar.addAction(action);
this._providerRelatedActions.push(action);
} else {
this._providerRelatedActions[foundIndex].enabled = true;
}
}
}
private setContextKeyServiceWithProviderId(providerId: string) {
let provider = new RawContextKey<string>('providerId', providerId);
provider.bindTo(this.contextKeyService);
}
public get notebookParams(): INotebookParams {
return this._notebookParams;
}
public get id(): string {
return this._notebookParams.notebookUri.toString();
}
public get modelReady(): Promise<INotebookModel> {
return this._modelReadyDeferred.promise;
}
isActive(): boolean {
return this.editorService.activeEditor === this.notebookParams.input;
}
isVisible(): boolean {
let notebookEditor = this.notebookParams.input;
return this.editorService.visibleEditors.some(e => e === notebookEditor);
}
isDirty(): boolean {
return this.notebookParams.input.isDirty();
}
executeEdits(edits: ISingleNotebookEditOperation[]): boolean {
if (!edits || edits.length === 0) {
return false;
}
this._model.pushEditOperations(edits);
return true;
}
public async runCell(cell: ICellModel): Promise<boolean> {
await this.modelReady;
let uriString = cell.cellUri.toString();
if (this._model.cells.findIndex(c => c.cellUri.toString() === uriString) > -1) {
this.selectCell(cell);
return cell.runCell(this.notificationService, this.connectionManagementService);
} else {
return Promise.reject(new Error(localize('cellNotFound', "cell with URI {0} was not found in this model", uriString)));
}
}
public async runAllCells(startCell?: ICellModel, endCell?: ICellModel): Promise<boolean> {
await this.modelReady;
let codeCells = this._model.cells.filter(cell => cell.cellType === CellTypes.Code);
if (codeCells && codeCells.length) {
// For the run all cells scenario where neither startId not endId are provided, set defaults
let startIndex = 0;
let endIndex = codeCells.length;
if (!isUndefinedOrNull(startCell)) {
startIndex = codeCells.findIndex(c => c.id === startCell.id);
}
if (!isUndefinedOrNull(endCell)) {
endIndex = codeCells.findIndex(c => c.id === endCell.id);
}
for (let i = startIndex; i < endIndex; i++) {
let cellStatus = await this.runCell(codeCells[i]);
if (!cellStatus) {
return Promise.reject(new Error(localize('cellRunFailed', "Run Cells failed - See error in output of the currently selected cell for more information.")));
}
}
}
return true;
}
public async clearOutput(cell: ICellModel): Promise<boolean> {
try {
await this.modelReady;
let uriString = cell.cellUri.toString();
if (this._model.cells.findIndex(c => c.cellUri.toString() === uriString) > -1) {
this.selectCell(cell);
// Clear outputs of the requested cell if cell type is code cell.
// If cell is markdown cell, clearOutputs() is a no-op
if (cell.cellType === CellTypes.Code) {
(cell as CellModel).clearOutputs();
}
return true;
} else {
return Promise.reject(new Error(localize('cellNotFound', "cell with URI {0} was not found in this model", uriString)));
}
} catch (e) {
return Promise.reject(e);
}
}
public async clearAllOutputs(): Promise<boolean> {
try {
await this.modelReady;
this._model.cells.forEach(cell => {
if (cell.cellType === CellTypes.Code) {
(cell as CellModel).clearOutputs();
}
});
return Promise.resolve(true);
}
catch (e) {
return Promise.reject(e);
}
}
public async nextPage(): Promise<void> {
try {
if (this._navProvider) {
this._navProvider.onNext(this.model.notebookUri);
}
} catch (error) {
this.notificationService.error(getErrorMessage(error));
}
}
public previousPage() {
try {
if (this._navProvider) {
this._navProvider.onPrevious(this.model.notebookUri);
}
} catch (error) {
this.notificationService.error(getErrorMessage(error));
}
}
getSections(): INotebookSection[] {
return this.getSectionElements();
}
private getSectionElements(): NotebookSection[] {
let headers: NotebookSection[] = [];
let el: HTMLElement = this.container.nativeElement;
let headerElements = el.querySelectorAll('h1, h2, h3, h4, h5, h6');
for (let i = 0; i < headerElements.length; i++) {
let headerEl = headerElements[i] as HTMLElement;
if (headerEl['id']) {
headers.push(new NotebookSection(headerEl));
}
}
return headers;
}
navigateToSection(id: string): void {
id = id.toLowerCase();
let section = this.getSectionElements().find(s => s.relativeUri && s.relativeUri.toLowerCase() === id);
if (section) {
// Scroll this section to the top of the header instead of just bringing header into view.
let scrollTop = jQuery(section.headerEl).offset().top;
(<HTMLElement>this.container.nativeElement).scrollTo({
top: scrollTop,
behavior: 'smooth'
});
section.headerEl.focus();
}
}
}
class NotebookSection implements INotebookSection {
constructor(public headerEl: HTMLElement) {
}
get relativeUri(): string {
return this.headerEl['id'];
}
get header(): string {
return this.headerEl.textContent;
}
}

View File

@@ -0,0 +1,210 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Registry } from 'vs/platform/registry/common/platform';
import { EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions';
import { SyncActionDescriptor, registerAction } from 'vs/platform/actions/common/actions';
import { NotebookInput } from 'sql/workbench/parts/notebook/common/models/notebookInput';
import { NotebookEditor } from 'sql/workbench/parts/notebook/browser/notebookEditor';
import { NewNotebookAction } from 'sql/workbench/parts/notebook/browser/notebookActions';
import { KeyMod } from 'vs/editor/common/standalone/standaloneBase';
import { KeyCode } from 'vs/base/common/keyCodes';
import { IConfigurationRegistry, Extensions as ConfigExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { localize } from 'vs/nls';
import { GridOutputComponent } from 'sql/workbench/parts/notebook/browser/outputs/gridOutput.component';
import { PlotlyOutputComponent } from 'sql/workbench/parts/notebook/browser/outputs/plotlyOutput.component';
import { registerComponentType } from 'sql/workbench/parts/notebook/browser/outputs/mimeRegistry';
import { MimeRendererComponent } from 'sql/workbench/parts/notebook/browser/outputs/mimeRenderer.component';
import { MarkdownOutputComponent } from 'sql/workbench/parts/notebook/browser/outputs/markdownOutput.component';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { URI } from 'vs/base/common/uri';
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
import { IWindowService } from 'vs/platform/windows/common/windows';
// Model View editor registration
const viewModelEditorDescriptor = new EditorDescriptor(
NotebookEditor,
NotebookEditor.ID,
'Notebook'
);
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
.registerEditor(viewModelEditorDescriptor, [new SyncDescriptor(NotebookInput)]);
// Global Actions
let actionRegistry = <IWorkbenchActionRegistry>Registry.as(Extensions.WorkbenchActions);
actionRegistry.registerWorkbenchAction(
new SyncActionDescriptor(
NewNotebookAction,
NewNotebookAction.ID,
NewNotebookAction.LABEL,
{ primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.KEY_N },
),
NewNotebookAction.LABEL
);
registerAction({
id: 'workbench.action.setWorkspaceAndOpen',
handler: async (accessor, options: { forceNewWindow: boolean, folderPath: URI }) => {
const viewletService = accessor.get(IViewletService);
const workspaceEditingService = accessor.get(IWorkspaceEditingService);
const windowService = accessor.get(IWindowService);
let folders = [];
if (!options.folderPath) {
return;
}
folders.push(options.folderPath);
await workspaceEditingService.addFolders(folders.map(folder => ({ uri: folder })));
await viewletService.openViewlet(viewletService.getDefaultViewletId(), true);
if (options.forceNewWindow) {
return windowService.openWindow([{ folderUri: folders[0] }], { forceNewWindow: options.forceNewWindow });
}
else {
return windowService.reloadWindow();
}
}
});
const configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': 'notebook',
'title': 'Notebook',
'type': 'object',
'properties': {
'notebook.useInProcMarkdown': {
'type': 'boolean',
'default': true,
'description': localize('notebook.inProcMarkdown', "Use in-process markdown viewer to render text cells more quickly (Experimental).")
}
}
});
/* *************** Output components *************** */
// Note: most existing types use the same component to render. In order to
// preserve correct rank order, we register it once for each different rank of
// MIME types.
/**
* A mime renderer component for raw html.
*/
registerComponentType({
mimeTypes: ['text/html'],
rank: 50,
safe: true,
ctor: MimeRendererComponent,
selector: MimeRendererComponent.SELECTOR
});
/**
* A mime renderer component for images.
*/
registerComponentType({
mimeTypes: ['image/bmp', 'image/png', 'image/jpeg', 'image/gif'],
rank: 90,
safe: true,
ctor: MimeRendererComponent,
selector: MimeRendererComponent.SELECTOR
});
/**
* A mime renderer component for svg.
*/
registerComponentType({
mimeTypes: ['image/svg+xml'],
rank: 80,
safe: false,
ctor: MimeRendererComponent,
selector: MimeRendererComponent.SELECTOR
});
/**
* A mime renderer component for plain and jupyter console text data.
*/
registerComponentType({
mimeTypes: [
'text/plain',
'application/vnd.jupyter.stdout',
'application/vnd.jupyter.stderr'
],
rank: 120,
safe: true,
ctor: MimeRendererComponent,
selector: MimeRendererComponent.SELECTOR
});
/**
* A placeholder component for deprecated rendered JavaScript.
*/
registerComponentType({
mimeTypes: ['text/javascript', 'application/javascript'],
rank: 110,
safe: false,
ctor: MimeRendererComponent,
selector: MimeRendererComponent.SELECTOR
});
/**
* A mime renderer component for grid data.
* This will be replaced by a dedicated component in the future
*/
registerComponentType({
mimeTypes: [
'application/vnd.dataresource+json',
'application/vnd.dataresource'
],
rank: 40,
safe: true,
ctor: GridOutputComponent,
selector: GridOutputComponent.SELECTOR
});
/**
* A mime renderer component for LaTeX.
*/
registerComponentType({
mimeTypes: ['text/latex'],
rank: 70,
safe: true,
ctor: MimeRendererComponent,
selector: MimeRendererComponent.SELECTOR
});
/**
* A mime renderer component for Markdown.
*/
registerComponentType({
mimeTypes: ['text/markdown'],
rank: 60,
safe: true,
ctor: MarkdownOutputComponent,
selector: MarkdownOutputComponent.SELECTOR
});
/**
* A mime renderer component for Plotly graphs.
*/
registerComponentType({
mimeTypes: ['application/vnd.plotly.v1+json'],
rank: 45,
safe: true,
ctor: PlotlyOutputComponent,
selector: PlotlyOutputComponent.SELECTOR
});
/**
* A mime renderer component for Plotly HTML output
* that will ensure this gets ignored if possible since it's only output
* on offline init and adds a <script> tag which does what we've done (add Plotly support into the app)
*/
registerComponentType({
mimeTypes: ['text/vnd.plotly.v1+html'],
rank: 46,
safe: true,
ctor: PlotlyOutputComponent,
selector: PlotlyOutputComponent.SELECTOR
});

View File

@@ -0,0 +1,88 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { forwardRef, NgModule, ComponentFactoryResolver, Inject, ApplicationRef } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CommonModule, APP_BASE_HREF } from '@angular/common';
import { BrowserModule } from '@angular/platform-browser';
import { ComponentHostDirective } from 'sql/workbench/parts/dashboard/browser/core/componentHost.directive';
import { providerIterator } from 'sql/platform/bootstrap/browser/bootstrapService';
import { CommonServiceInterface } from 'sql/platform/bootstrap/browser/commonServiceInterface.service';
import { EditableDropDown } from 'sql/platform/browser/editableDropdown/editableDropdown.component';
import { NotebookComponent } from 'sql/workbench/parts/notebook/browser/notebook.component';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { CodeComponent } from 'sql/workbench/parts/notebook/browser/cellViews/code.component';
import { CodeCellComponent } from 'sql/workbench/parts/notebook/browser/cellViews/codeCell.component';
import { TextCellComponent } from 'sql/workbench/parts/notebook/browser/cellViews/textCell.component';
import { OutputAreaComponent } from 'sql/workbench/parts/notebook/browser/cellViews/outputArea.component';
import { OutputComponent } from 'sql/workbench/parts/notebook/browser/cellViews/output.component';
import { StdInComponent } from 'sql/workbench/parts/notebook/browser/cellViews/stdin.component';
import { PlaceholderCellComponent } from 'sql/workbench/parts/notebook/browser/cellViews/placeholderCell.component';
import LoadingSpinner from 'sql/workbench/browser/modelComponents/loadingSpinner.component';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox.component';
import { SelectBox } from 'sql/platform/browser/selectBox/selectBox.component';
import { InputBox } from 'sql/platform/browser/inputbox/inputBox.component';
import { IMimeComponentRegistry, Extensions } from 'sql/workbench/parts/notebook/browser/outputs/mimeRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { LinkHandlerDirective } from 'sql/workbench/parts/notebook/browser/cellViews/linkHandler.directive';
import { IBootstrapParams, ISelector } from 'sql/platform/bootstrap/common/bootstrapParams';
export const NotebookModule = (params, selector: string, instantiationService: IInstantiationService): any => {
let outputComponents = Registry.as<IMimeComponentRegistry>(Extensions.MimeComponentContribution).getAllCtors();
@NgModule({
declarations: [
Checkbox,
SelectBox,
EditableDropDown,
InputBox,
LoadingSpinner,
CodeComponent,
CodeCellComponent,
TextCellComponent,
PlaceholderCellComponent,
NotebookComponent,
ComponentHostDirective,
OutputAreaComponent,
OutputComponent,
StdInComponent,
LinkHandlerDirective,
...outputComponents
],
entryComponents: [
NotebookComponent,
...outputComponents
],
imports: [
FormsModule,
CommonModule,
BrowserModule
],
providers: [
{ provide: APP_BASE_HREF, useValue: '/' },
CommonServiceInterface,
{ provide: IBootstrapParams, useValue: params },
{ provide: ISelector, useValue: selector },
...providerIterator(instantiationService)
]
})
class ModuleClass {
constructor(
@Inject(forwardRef(() => ComponentFactoryResolver)) private _resolver: ComponentFactoryResolver,
@Inject(ISelector) private selector: string
) {
}
ngDoBootstrap(appRef: ApplicationRef) {
const factoryWrapper: any = this._resolver.resolveComponentFactory(NotebookComponent);
factoryWrapper.factory.selector = this.selector;
appRef.bootstrap(factoryWrapper);
}
}
return ModuleClass;
};

View File

@@ -0,0 +1,563 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { Action } from 'vs/base/common/actions';
import { localize } from 'vs/nls';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { INotificationService, Severity, INotificationActions } from 'vs/platform/notification/common/notification';
import { SelectBox, ISelectBoxOptionsWithLabel } from 'sql/base/browser/ui/selectBox/selectBox';
import { IConnectionManagementService, ConnectionType } from 'sql/platform/connection/common/connectionManagement';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { noKernel } from 'sql/workbench/services/notebook/common/sessionManager';
import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService';
import { NotebookModel } from 'sql/workbench/parts/notebook/common/models/notebookModel';
import { generateUri } from 'sql/platform/connection/common/utils';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ILogService } from 'vs/platform/log/common/log';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { CellType } from 'sql/workbench/parts/notebook/common/models/contracts';
import { NotebookComponent } from 'sql/workbench/parts/notebook/browser/notebook.component';
import { getErrorMessage } from 'vs/base/common/errors';
import { INotebookModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
const msgLoading = localize('loading', "Loading kernels...");
const msgChanging = localize('changing', "Changing kernel...");
const kernelLabel: string = localize('Kernel', "Kernel: ");
const attachToLabel: string = localize('AttachTo', "Attach To: ");
const msgLoadingContexts = localize('loadingContexts', "Loading contexts...");
const msgAddNewConnection = localize('addNewConnection', "Add New Connection");
const msgSelectConnection = localize('selectConnection', "Select Connection");
const msgLocalHost = localize('localhost', "localhost");
const HIDE_ICON_CLASS = ' hideIcon';
// Action to add a cell to notebook based on cell type(code/markdown).
export class AddCellAction extends Action {
public cellType: CellType;
constructor(
id: string, label: string, cssClass: string
) {
super(id, label, cssClass);
}
public run(context: NotebookComponent): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
try {
context.addCell(this.cellType);
resolve(true);
} catch (e) {
reject(e);
}
});
}
}
// Action to clear outputs of all code cells.
export class ClearAllOutputsAction extends Action {
constructor(
id: string, label: string, cssClass: string
) {
super(id, label, cssClass);
}
public run(context: NotebookComponent): Promise<boolean> {
return context.clearAllOutputs();
}
}
export interface IToggleableState {
baseClass?: string;
shouldToggleTooltip?: boolean;
toggleOnClass: string;
toggleOnLabel: string;
toggleOffLabel: string;
toggleOffClass: string;
isOn: boolean;
}
export abstract class ToggleableAction extends Action {
constructor(id: string, protected state: IToggleableState) {
super(id, '');
this.updateLabelAndIcon();
}
private updateLabelAndIcon() {
if (this.state.shouldToggleTooltip) {
this.tooltip = this.state.isOn ? this.state.toggleOnLabel : this.state.toggleOffLabel;
} else {
this.label = this.state.isOn ? this.state.toggleOnLabel : this.state.toggleOffLabel;
}
let classes = this.state.baseClass ? `${this.state.baseClass} ` : '';
classes += this.state.isOn ? this.state.toggleOnClass : this.state.toggleOffClass;
this.class = classes;
}
protected toggle(isOn: boolean): void {
this.state.isOn = isOn;
this.updateLabelAndIcon();
}
}
export interface IActionStateData {
className?: string;
label?: string;
tooltip?: string;
hideIcon?: boolean;
commandId?: string;
}
export class IMultiStateData<T> {
private _stateMap = new Map<T, IActionStateData>();
constructor(mappings: { key: T, value: IActionStateData }[], private _state: T, private _baseClass?: string) {
if (mappings) {
mappings.forEach(s => this._stateMap.set(s.key, s.value));
}
}
public set state(value: T) {
if (!this._stateMap.has(value)) {
throw new Error('State value must be in stateMap');
}
this._state = value;
}
public updateStateData(state: T, updater: (data: IActionStateData) => void): void {
let data = this._stateMap.get(state);
if (data) {
updater(data);
}
}
public get classes(): string {
let classVal = this.getStateValueOrDefault<string>((data) => data.className, '');
let classes = this._baseClass ? `${this._baseClass} ` : '';
classes += classVal;
if (this.getStateValueOrDefault<boolean>((data) => data.hideIcon, false)) {
classes += HIDE_ICON_CLASS;
}
return classes;
}
public get label(): string {
return this.getStateValueOrDefault<string>((data) => data.label, '');
}
public get tooltip(): string {
return this.getStateValueOrDefault<string>((data) => data.tooltip, '');
}
public get commandId(): string {
return this.getStateValueOrDefault<string>((data) => data.commandId, '');
}
private getStateValueOrDefault<U>(getter: (data: IActionStateData) => U, defaultVal?: U): U {
let data = this._stateMap.get(this._state);
return data ? getter(data) : defaultVal;
}
}
export abstract class MultiStateAction<T> extends Action {
constructor(
id: string,
protected states: IMultiStateData<T>,
private _keybindingService: IKeybindingService,
private readonly logService: ILogService) {
super(id, '');
this.updateLabelAndIcon();
}
private updateLabelAndIcon() {
let keyboardShortcut: string;
try {
// If a keyboard shortcut exists for the command id passed in, append that to the label
if (this.states.commandId !== '') {
let binding = this._keybindingService.lookupKeybinding(this.states.commandId);
keyboardShortcut = binding ? binding.getLabel() : undefined;
}
} catch (error) {
this.logService.error(error);
}
this.label = this.states.label;
this.tooltip = keyboardShortcut ? this.states.tooltip + ` (${keyboardShortcut})` : this.states.tooltip;
this.class = this.states.classes;
}
protected updateState(state: T): void {
this.states.state = state;
this.updateLabelAndIcon();
}
}
export class TrustedAction extends ToggleableAction {
// Constants
private static readonly trustedLabel = localize('trustLabel', "Trusted");
private static readonly notTrustedLabel = localize('untrustLabel', "Not Trusted");
private static readonly alreadyTrustedMsg = localize('alreadyTrustedMsg', "Notebook is already trusted.");
private static readonly baseClass = 'notebook-button';
private static readonly trustedCssClass = 'icon-trusted';
private static readonly notTrustedCssClass = 'icon-notTrusted';
// Properties
constructor(
id: string,
@INotificationService private _notificationService: INotificationService
) {
super(id, {
baseClass: TrustedAction.baseClass,
toggleOnLabel: TrustedAction.trustedLabel,
toggleOnClass: TrustedAction.trustedCssClass,
toggleOffLabel: TrustedAction.notTrustedLabel,
toggleOffClass: TrustedAction.notTrustedCssClass,
isOn: false
});
}
public get trusted(): boolean {
return this.state.isOn;
}
public set trusted(value: boolean) {
this.toggle(value);
}
public run(context: NotebookComponent): Promise<boolean> {
let self = this;
return new Promise<boolean>((resolve, reject) => {
try {
if (self.trusted) {
const actions: INotificationActions = { primary: [] };
self._notificationService.notify({ severity: Severity.Info, message: TrustedAction.alreadyTrustedMsg, actions });
}
else {
self.trusted = !self.trusted;
context.model.trustedMode = self.trusted;
}
resolve(true);
} catch (e) {
reject(e);
}
});
}
}
// Action to run all code cells in a notebook.
export class RunAllCellsAction extends Action {
constructor(
id: string, label: string, cssClass: string,
@INotificationService private notificationService: INotificationService
) {
super(id, label, cssClass);
}
public async run(context: NotebookComponent): Promise<boolean> {
try {
await context.runAllCells();
return true;
} catch (e) {
this.notificationService.error(getErrorMessage(e));
return false;
}
}
}
export class KernelsDropdown extends SelectBox {
private model: NotebookModel;
constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, modelReady: Promise<INotebookModel>) {
super([msgLoading], msgLoading, contextViewProvider, container, { labelText: kernelLabel, labelOnTop: false, ariaLabel: kernelLabel } as ISelectBoxOptionsWithLabel);
if (modelReady) {
modelReady
.then((model) => this.updateModel(model))
.catch((err) => {
// No-op for now
});
}
this.onDidSelect(e => this.doChangeKernel(e.selected));
}
updateModel(model: INotebookModel): void {
this.model = model as NotebookModel;
this._register(this.model.kernelChanged((changedArgs: azdata.nb.IKernelChangedArgs) => {
this.updateKernel(changedArgs.newValue);
}));
let kernel = this.model.clientSession && this.model.clientSession.kernel;
this.updateKernel(kernel);
}
// Update SelectBox values
public updateKernel(kernel: azdata.nb.IKernel) {
let kernels: string[] = this.model.standardKernelsDisplayName();
if (kernel && kernel.isReady) {
let standardKernel = this.model.getStandardKernelFromName(kernel.name);
if (kernels && standardKernel) {
let index = kernels.findIndex((kernel => kernel === standardKernel.displayName));
this.setOptions(kernels, index);
}
} else if (this.model.clientSession.isInErrorState) {
let noKernelName = localize('noKernel', "No Kernel");
kernels.unshift(noKernelName);
this.setOptions(kernels, 0);
}
}
public doChangeKernel(displayName: string): void {
this.setOptions([msgChanging], 0);
this.model.changeKernel(displayName);
}
}
export class AttachToDropdown extends SelectBox {
private model: NotebookModel;
constructor(
container: HTMLElement, contextViewProvider: IContextViewProvider, modelReady: Promise<INotebookModel>,
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
@IConnectionDialogService private _connectionDialogService: IConnectionDialogService,
@INotificationService private _notificationService: INotificationService,
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService,
@ILogService private readonly logService: ILogService
) {
super([msgLoadingContexts], msgLoadingContexts, contextViewProvider, container, { labelText: attachToLabel, labelOnTop: false, ariaLabel: attachToLabel } as ISelectBoxOptionsWithLabel);
if (modelReady) {
modelReady
.then(model => {
this.updateModel(model);
this.updateAttachToDropdown(model);
})
.catch(err => {
// No-op for now
});
}
this.onDidSelect(e => {
this.doChangeContext(this.getSelectedConnection(e.selected));
});
}
public updateModel(model: INotebookModel): void {
this.model = model as NotebookModel;
this._register(model.contextsChanged(() => {
this.handleContextsChanged();
}));
this._register(this.model.contextsLoading(() => {
this.setOptions([msgLoadingContexts], 0);
}));
this.model.requestConnectionHandler = () => this.openConnectionDialog(true);
this.handleContextsChanged();
}
private handleContextsChanged(showSelectConnection?: boolean) {
let kernelDisplayName: string = this.getKernelDisplayName();
if (kernelDisplayName) {
this.loadAttachToDropdown(this.model, kernelDisplayName, showSelectConnection);
} else if (this.model.clientSession.isInErrorState) {
this.setOptions([localize('noContextAvailable', "None")], 0);
}
}
private updateAttachToDropdown(model: INotebookModel): void {
if (this.model.connectionProfile && this.model.connectionProfile.serverName) {
let connectionUri = generateUri(this.model.connectionProfile, 'notebook');
this.model.notebookOptions.connectionService.connect(this.model.connectionProfile, connectionUri).then(result => {
if (result.connected) {
let connectionProfile = new ConnectionProfile(this._capabilitiesService, result.connectionProfile);
this.model.addAttachToConnectionsToBeDisposed(connectionUri);
this.doChangeContext(connectionProfile);
} else {
this.openConnectionDialog(true);
}
}).catch(err =>
this.logService.error(err));
}
this._register(model.onValidConnectionSelected(validConnection => {
this.handleContextsChanged(!validConnection);
}));
}
private getKernelDisplayName(): string {
let kernelDisplayName: string;
if (this.model.clientSession && this.model.clientSession.kernel && this.model.clientSession.kernel.name) {
let currentKernelName = this.model.clientSession.kernel.name.toLowerCase();
let currentKernelSpec = this.model.specs.kernels.find(kernel => kernel.name && kernel.name.toLowerCase() === currentKernelName);
if (currentKernelSpec) {
kernelDisplayName = currentKernelSpec.display_name;
}
}
return kernelDisplayName;
}
// Load "Attach To" dropdown with the values corresponding to Kernel dropdown
public async loadAttachToDropdown(model: INotebookModel, currentKernel: string, showSelectConnection?: boolean): Promise<void> {
let connProviderIds = this.model.getApplicableConnectionProviderIds(currentKernel);
if ((connProviderIds && connProviderIds.length === 0) || currentKernel === noKernel) {
this.setOptions([msgLocalHost]);
}
else {
let connections = this.getConnections(model);
this.enable();
if (showSelectConnection) {
connections = this.loadWithSelectConnection(connections);
}
else {
if (connections.length === 1 && connections[0] === msgAddNewConnection) {
connections.unshift(msgSelectConnection);
}
else {
if (!connections.includes(msgAddNewConnection)) {
connections.push(msgAddNewConnection);
}
}
this.setOptions(connections, 0);
}
}
}
private loadWithSelectConnection(connections: string[]): string[] {
if (connections && connections.length > 0) {
if (!connections.includes(msgSelectConnection)) {
connections.unshift(msgSelectConnection);
}
if (!connections.includes(msgAddNewConnection)) {
connections.push(msgAddNewConnection);
}
this.setOptions(connections, 0);
}
return connections;
}
//Get connections from context
public getConnections(model: INotebookModel): string[] {
let otherConnections: ConnectionProfile[] = [];
model.contexts.otherConnections.forEach((conn) => { otherConnections.push(conn); });
// If current connection connects to master, select the option in the dropdown that doesn't specify a database
if (!model.contexts.defaultConnection.databaseName) {
this.selectWithOptionName(model.contexts.defaultConnection.serverName);
} else {
if (model.contexts.defaultConnection) {
this.selectWithOptionName(model.contexts.defaultConnection.title ? model.contexts.defaultConnection.title : model.contexts.defaultConnection.serverName);
} else {
this.select(0);
}
}
otherConnections = this.setConnectionsList(model.contexts.defaultConnection, model.contexts.otherConnections);
let connections = otherConnections.map((context) => context.title ? context.title : context.serverName);
return connections;
}
private setConnectionsList(defaultConnection: ConnectionProfile, otherConnections: ConnectionProfile[]) {
if (defaultConnection.serverName !== msgSelectConnection) {
otherConnections = otherConnections.filter(conn => conn.id !== defaultConnection.id);
otherConnections.unshift(defaultConnection);
if (otherConnections.length > 1) {
otherConnections = otherConnections.filter(val => val.serverName !== msgSelectConnection);
}
}
return otherConnections;
}
public getSelectedConnection(selection: string): ConnectionProfile {
// Find all connections with the the same server as the selected option
let connections = this.model.contexts.otherConnections.filter((c) => selection === c.title);
// If only one connection exists with the same server name, use that one
if (connections.length === 1) {
return connections[0];
} else {
return this.model.contexts.otherConnections.find((c) => selection === c.title);
}
}
public doChangeContext(connection?: ConnectionProfile, hideErrorMessage?: boolean): void {
if (this.value === msgAddNewConnection) {
this.openConnectionDialog();
} else {
this.model.changeContext(this.value, connection, hideErrorMessage).then(ok => undefined, err => this._notificationService.error(getErrorMessage(err)));
}
}
/**
* Open connection dialog
* Enter server details and connect to a server from the dialog
* Bind the server value to 'Attach To' drop down
* Connected server is displayed at the top of drop down
**/
public async openConnectionDialog(useProfile: boolean = false): Promise<boolean> {
try {
let connection = await this._connectionDialogService.openDialogAndWait(this._connectionManagementService,
{
connectionType: ConnectionType.temporary,
providers: this.model.getApplicableConnectionProviderIds(this.model.clientSession.kernel.name)
},
useProfile ? this.model.connectionProfile : undefined);
let attachToConnections = this.values;
if (!connection) {
this.loadAttachToDropdown(this.model, this.getKernelDisplayName());
this.doChangeContext(undefined, true);
return false;
}
let connectionUri = this._connectionManagementService.getConnectionUri(connection);
let connectionProfile = new ConnectionProfile(this._capabilitiesService, connection);
let connectedServer = connectionProfile.title ? connectionProfile.title : connectionProfile.serverName;
//Check to see if the same server is already there in dropdown. We only have server names in dropdown
if (attachToConnections.some(val => val === connectedServer)) {
this.loadAttachToDropdown(this.model, this.getKernelDisplayName());
this.doChangeContext();
return true;
}
else {
attachToConnections.unshift(connectedServer);
}
//To ignore n/a after we have at least one valid connection
attachToConnections = attachToConnections.filter(val => val !== msgSelectConnection);
let index = attachToConnections.findIndex((connection => connection === connectedServer));
this.setOptions([]);
this.setOptions(attachToConnections);
if (!index || index < 0 || index >= attachToConnections.length) {
index = 0;
}
this.select(index);
this.model.addAttachToConnectionsToBeDisposed(connectionUri);
// Call doChangeContext to set the newly chosen connection in the model
this.doChangeContext(connectionProfile);
return true;
}
catch (error) {
const actions: INotificationActions = { primary: [] };
this._notificationService.notify({ severity: Severity.Error, message: getErrorMessage(error), actions });
return false;
}
}
}
export class NewNotebookAction extends Action {
public static readonly ID = 'notebook.command.new';
public static readonly LABEL = localize('newNotebookAction', "New Notebook");
private static readonly INTERNAL_NEW_NOTEBOOK_CMD_ID = '_notebook.command.new';
constructor(
id: string,
label: string,
@ICommandService private commandService: ICommandService
) {
super(id, label);
this.class = 'notebook-action new-notebook';
}
run(context?: azdata.ConnectedContext): Promise<void> {
return this.commandService.executeCommand(NewNotebookAction.INTERNAL_NEW_NOTEBOOK_CMD_ID, context);
}
}

View File

@@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorOptions } from 'vs/workbench/common/editor';
import * as DOM from 'vs/base/browser/dom';
import { bootstrapAngular } from 'sql/platform/bootstrap/browser/bootstrapService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { CancellationToken } from 'vs/base/common/cancellation';
import { NotebookInput } from 'sql/workbench/parts/notebook/common/models/notebookInput';
import { NotebookModule } from 'sql/workbench/parts/notebook/browser/notebook.module';
import { NOTEBOOK_SELECTOR } from 'sql/workbench/parts/notebook/browser/notebook.component';
import { INotebookParams } from 'sql/workbench/services/notebook/common/notebookService';
import { IStorageService } from 'vs/platform/storage/common/storage';
export class NotebookEditor extends BaseEditor {
public static ID: string = 'workbench.editor.notebookEditor';
private _notebookContainer: HTMLElement;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IInstantiationService private instantiationService: IInstantiationService,
@IStorageService storageService: IStorageService
) {
super(NotebookEditor.ID, telemetryService, themeService, storageService);
}
public get notebookInput(): NotebookInput {
return this.input as NotebookInput;
}
/**
* Called to create the editor in the parent element.
*/
public createEditor(parent: HTMLElement): void {
}
/**
* Sets focus on this editor. Specifically, it sets the focus on the hosted text editor.
*/
public focus(): void {
}
/**
* Updates the internal variable keeping track of the editor's size, and re-calculates the sash position.
* To be called when the container of this editor changes size.
*/
public layout(dimension: DOM.Dimension): void {
if (this.notebookInput) {
this.notebookInput.doChangeLayout();
}
}
public setInput(input: NotebookInput, options: EditorOptions): Promise<void> {
if (this.input && this.input.matches(input)) {
return Promise.resolve(undefined);
}
const parentElement = this.getContainer();
super.setInput(input, options, CancellationToken.None);
DOM.clearNode(parentElement);
if (!input.hasBootstrapped) {
let container = DOM.$<HTMLElement>('.notebookEditor');
container.style.height = '100%';
this._notebookContainer = DOM.append(parentElement, container);
input.container = this._notebookContainer;
return Promise.resolve(this.bootstrapAngular(input));
} else {
this._notebookContainer = DOM.append(parentElement, input.container);
input.doChangeLayout();
return Promise.resolve(null);
}
}
/**
* Load the angular components and record for this input that we have done so
*/
private bootstrapAngular(input: NotebookInput): void {
// Get the bootstrap params and perform the bootstrap
input.hasBootstrapped = true;
let params: INotebookParams = {
notebookUri: input.notebookUri,
input: input,
providerInfo: input.getProviderInfo(),
profile: input.connectionProfile
};
bootstrapAngular(this.instantiationService,
NotebookModule,
this._notebookContainer,
NOTEBOOK_SELECTOR,
params,
input
);
}
}

View File

@@ -0,0 +1,95 @@
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
import * as widgets from './widgets';
import { IRenderMime } from '../../common/models/renderMimeInterfaces';
/**
* A mime renderer factory for raw html.
*/
export const htmlRendererFactory: IRenderMime.IRendererFactory = {
safe: true,
mimeTypes: ['text/html'],
defaultRank: 50,
createRenderer: options => new widgets.RenderedHTML(options)
};
/**
* A mime renderer factory for images.
*/
export const imageRendererFactory: IRenderMime.IRendererFactory = {
safe: true,
mimeTypes: ['image/bmp', 'image/png', 'image/jpeg', 'image/gif'],
defaultRank: 90,
createRenderer: options => new widgets.RenderedImage(options)
};
// /**
// * A mime renderer factory for LaTeX.
// */
// export const latexRendererFactory: IRenderMime.IRendererFactory = {
// safe: true,
// mimeTypes: ['text/latex'],
// defaultRank: 70,
// createRenderer: options => new widgets.RenderedLatex(options)
// };
/**
* A mime renderer factory for svg.
*/
export const svgRendererFactory: IRenderMime.IRendererFactory = {
safe: false,
mimeTypes: ['image/svg+xml'],
defaultRank: 80,
createRenderer: options => new widgets.RenderedSVG(options)
};
/**
* A mime renderer factory for plain and jupyter console text data.
*/
export const textRendererFactory: IRenderMime.IRendererFactory = {
safe: true,
mimeTypes: [
'text/plain',
'application/vnd.jupyter.stdout',
'application/vnd.jupyter.stderr'
],
defaultRank: 120,
createRenderer: options => new widgets.RenderedText(options)
};
/**
* A placeholder factory for deprecated rendered JavaScript.
*/
export const javaScriptRendererFactory: IRenderMime.IRendererFactory = {
safe: false,
mimeTypes: ['text/javascript', 'application/javascript'],
defaultRank: 110,
createRenderer: options => new widgets.RenderedJavaScript(options)
};
export const dataResourceRendererFactory: IRenderMime.IRendererFactory = {
safe: true,
mimeTypes: [
'application/vnd.dataresource+json',
'application/vnd.dataresource'
],
defaultRank: 40,
createRenderer: options => new widgets.RenderedDataResource(options)
};
/**
* The standard factories provided by the rendermime package.
*/
export const standardRendererFactories: ReadonlyArray<IRenderMime.IRendererFactory> = [
htmlRendererFactory,
// latexRendererFactory,
svgRendererFactory,
imageRendererFactory,
javaScriptRendererFactory,
textRendererFactory,
dataResourceRendererFactory
];

View File

@@ -0,0 +1,283 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { OnInit, Component, Input, Inject, ViewChild, ElementRef } from '@angular/core';
import * as azdata from 'azdata';
import { IGridDataProvider, getResultsString } from 'sql/platform/query/common/gridDataProvider';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { SaveFormat } from 'sql/workbench/parts/grid/common/interfaces';
import { IDataResource } from 'sql/workbench/services/notebook/sql/sqlSessionManager';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
import { getEolString, shouldIncludeHeaders, shouldRemoveNewLines } from 'sql/platform/query/common/queryRunner';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { attachTableStyler } from 'sql/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { localize } from 'vs/nls';
import { IAction } from 'vs/base/common/actions';
import { AngularDisposable } from 'sql/base/browser/lifecycle';
import { IMimeComponent } from 'sql/workbench/parts/notebook/browser/outputs/mimeRegistry';
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import { MimeModel } from 'sql/workbench/parts/notebook/common/models/mimemodel';
import { GridTableState } from 'sql/workbench/parts/query/common/gridPanelState';
import { GridTableBase } from 'sql/workbench/parts/query/browser/gridPanel';
import { getErrorMessage } from 'vs/base/common/errors';
@Component({
selector: GridOutputComponent.SELECTOR,
template: `<div #output class="notebook-cellTable"></div>`
})
export class GridOutputComponent extends AngularDisposable implements IMimeComponent, OnInit {
public static readonly SELECTOR: string = 'grid-output';
@ViewChild('output', { read: ElementRef }) private output: ElementRef;
private _initialized: boolean = false;
private _cellModel: ICellModel;
private _bundleOptions: MimeModel.IOptions;
private _table: DataResourceTable;
constructor(
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
@Inject(IThemeService) private readonly themeService: IThemeService
) {
super();
}
@Input() set bundleOptions(value: MimeModel.IOptions) {
this._bundleOptions = value;
if (this._initialized) {
this.renderGrid();
}
}
@Input() mimeType: string;
get cellModel(): ICellModel {
return this._cellModel;
}
@Input() set cellModel(value: ICellModel) {
this._cellModel = value;
if (this._initialized) {
this.renderGrid();
}
}
ngOnInit() {
this.renderGrid();
}
renderGrid(): void {
if (!this._bundleOptions || !this._cellModel || !this.mimeType) {
return;
}
if (!this._table) {
let source = <IDataResource><any>this._bundleOptions.data[this.mimeType];
let state = new GridTableState(0, 0);
this._table = this.instantiationService.createInstance(DataResourceTable, source, this.cellModel.notebookModel.notebookUri.toString(), state);
let outputElement = <HTMLElement>this.output.nativeElement;
outputElement.appendChild(this._table.element);
this._register(attachTableStyler(this._table, this.themeService));
this.layout();
this._table.onAdd();
this._initialized = true;
}
}
layout(): void {
if (this._table) {
let maxSize = Math.min(this._table.maximumSize, 500);
this._table.layout(maxSize);
}
}
}
class DataResourceTable extends GridTableBase<any> {
private _gridDataProvider: IGridDataProvider;
constructor(source: IDataResource,
documentUri: string,
state: GridTableState,
@IContextMenuService contextMenuService: IContextMenuService,
@IInstantiationService instantiationService: IInstantiationService,
@IEditorService editorService: IEditorService,
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
@IConfigurationService configurationService: IConfigurationService
) {
super(state, createResultSet(source), contextMenuService, instantiationService, editorService, untitledEditorService, configurationService);
this._gridDataProvider = this.instantiationService.createInstance(DataResourceDataProvider, source, this.resultSet, documentUri);
}
get gridDataProvider(): IGridDataProvider {
return this._gridDataProvider;
}
protected getCurrentActions(): IAction[] {
return [];
}
protected getContextActions(): IAction[] {
return [];
}
public get maximumSize(): number {
// Overriding action bar size calculation for now.
// When we add this back in, we should update this calculation
return Math.max(this.maxSize, /* ACTIONBAR_HEIGHT + BOTTOM_PADDING */ 0);
}
}
class DataResourceDataProvider implements IGridDataProvider {
private rows: azdata.DbCellValue[][];
constructor(source: IDataResource,
private resultSet: azdata.ResultSetSummary,
private documentUri: string,
@INotificationService private _notificationService: INotificationService,
@IClipboardService private _clipboardService: IClipboardService,
@IConfigurationService private _configurationService: IConfigurationService,
@ITextResourcePropertiesService private _textResourcePropertiesService: ITextResourcePropertiesService
) {
this.transformSource(source);
}
private transformSource(source: IDataResource): void {
this.rows = source.data.map(row => {
let rowData: azdata.DbCellValue[] = [];
Object.keys(row).forEach((val, index) => {
let displayValue = String(Object.values(row)[index]);
// Since the columns[0] represents the row number, start at 1
rowData.push({
displayValue: displayValue,
isNull: false,
invariantCultureDisplayValue: displayValue
});
});
return rowData;
});
}
getRowData(rowStart: number, numberOfRows: number): Thenable<azdata.QueryExecuteSubsetResult> {
let rowEnd = rowStart + numberOfRows;
if (rowEnd > this.rows.length) {
rowEnd = this.rows.length;
}
let resultSubset: azdata.QueryExecuteSubsetResult = {
message: undefined,
resultSubset: {
rowCount: rowEnd - rowStart,
rows: this.rows.slice(rowStart, rowEnd)
}
};
return Promise.resolve(resultSubset);
}
copyResults(selection: Slick.Range[], includeHeaders?: boolean): void {
this.copyResultsAsync(selection, includeHeaders);
}
private async copyResultsAsync(selection: Slick.Range[], includeHeaders?: boolean): Promise<void> {
try {
let results = await getResultsString(this, selection, includeHeaders);
this._clipboardService.writeText(results);
} catch (error) {
this._notificationService.error(localize('copyFailed', "Copy failed with error {0}", getErrorMessage(error)));
}
}
getEolString(): string {
return getEolString(this._textResourcePropertiesService, this.documentUri);
}
shouldIncludeHeaders(includeHeaders: boolean): boolean {
return shouldIncludeHeaders(includeHeaders, this._configurationService);
}
shouldRemoveNewLines(): boolean {
return shouldRemoveNewLines(this._configurationService);
}
getColumnHeaders(range: Slick.Range): string[] {
let headers: string[] = this.resultSet.columnInfo.slice(range.fromCell, range.toCell + 1).map((info, i) => {
return info.columnName;
});
return headers;
}
get canSerialize(): boolean {
return false;
}
serializeResults(format: SaveFormat, selection: Slick.Range[]): Thenable<void> {
throw new Error('Method not implemented.');
}
}
function createResultSet(source: IDataResource): azdata.ResultSetSummary {
let columnInfo: azdata.IDbColumn[] = source.schema.fields.map(field => {
let column = new SimpleDbColumn(field.name);
if (field.type) {
switch (field.type) {
case 'xml':
column.isXml = true;
break;
case 'json':
column.isJson = true;
break;
default:
// Only handling a few cases for now
break;
}
}
return column;
});
let summary: azdata.ResultSetSummary = {
batchId: 0,
id: 0,
complete: true,
rowCount: source.data.length,
columnInfo: columnInfo
};
return summary;
}
class SimpleDbColumn implements azdata.IDbColumn {
constructor(columnName: string) {
this.columnName = columnName;
}
allowDBNull?: boolean;
baseCatalogName: string;
baseColumnName: string;
baseSchemaName: string;
baseServerName: string;
baseTableName: string;
columnName: string;
columnOrdinal?: number;
columnSize?: number;
isAliased?: boolean;
isAutoIncrement?: boolean;
isExpression?: boolean;
isHidden?: boolean;
isIdentity?: boolean;
isKey?: boolean;
isBytes?: boolean;
isChars?: boolean;
isSqlVariant?: boolean;
isUdt?: boolean;
dataType: string;
isXml?: boolean;
isJson?: boolean;
isLong?: boolean;
isReadOnly?: boolean;
isUnique?: boolean;
numericPrecision?: number;
numericScale?: number;
udtAssemblyQualifiedName: string;
dataTypeName: string;
}

View File

@@ -0,0 +1,5 @@
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: row">
<div class="icon in-progress" *ngIf="loading === true"></div>
<div #output link-handler [isTrusted]="isTrusted" [notebookUri]="notebookUri" class="notebook-preview" style="flex: 1 1 auto">
</div>
</div>

View File

@@ -0,0 +1,154 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!../cellViews/textCell';
import 'vs/css!../cellViews/media/markdown';
import 'vs/css!../cellViews/media/highlight';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild } from '@angular/core';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { ISanitizer, defaultSanitizer } from 'sql/workbench/parts/notebook/browser/outputs/sanitizer';
import { AngularDisposable } from 'sql/base/browser/lifecycle';
import { IMimeComponent } from 'sql/workbench/parts/notebook/browser/outputs/mimeRegistry';
import { INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { NotebookMarkdownRenderer } from 'sql/workbench/parts/notebook/browser/outputs/notebookMarkdown';
import { MimeModel } from 'sql/workbench/parts/notebook/common/models/mimemodel';
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import { useInProcMarkdown, convertVscodeResourceToFileInSubDirectories } from 'sql/workbench/parts/notebook/common/models/notebookUtils';
import { URI } from 'vs/base/common/uri';
@Component({
selector: MarkdownOutputComponent.SELECTOR,
templateUrl: decodeURI(require.toUrl('./markdownOutput.component.html'))
})
export class MarkdownOutputComponent extends AngularDisposable implements IMimeComponent, OnInit {
public static readonly SELECTOR: string = 'markdown-output';
@ViewChild('output', { read: ElementRef }) private output: ElementRef;
private _sanitizer: ISanitizer;
private _lastTrustedMode: boolean;
private _bundleOptions: MimeModel.IOptions;
private _initialized: boolean = false;
public loading: boolean = false;
private _cellModel: ICellModel;
private _markdownRenderer: NotebookMarkdownRenderer;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(ICommandService) private _commandService: ICommandService,
@Inject(INotebookService) private _notebookService: INotebookService,
@Inject(IConfigurationService) private _configurationService: IConfigurationService,
@Inject(IInstantiationService) private _instantiationService: IInstantiationService
) {
super();
this._sanitizer = this._notebookService.getMimeRegistry().sanitizer;
this._markdownRenderer = this._instantiationService.createInstance(NotebookMarkdownRenderer);
}
@Input() set bundleOptions(value: MimeModel.IOptions) {
this._bundleOptions = value;
if (this._initialized) {
this.updatePreview();
}
}
@Input() mimeType: string;
get cellModel(): ICellModel {
return this._cellModel;
}
@Input() set cellModel(value: ICellModel) {
this._cellModel = value;
if (this._initialized) {
this.updatePreview();
}
}
public get isTrusted(): boolean {
return this._bundleOptions && this._bundleOptions.trusted;
}
public get notebookUri(): URI {
return this.cellModel.notebookModel.notebookUri;
}
//Gets sanitizer from ISanitizer interface
private get sanitizer(): ISanitizer {
if (this._sanitizer) {
return this._sanitizer;
}
return this._sanitizer = defaultSanitizer;
}
private setLoading(isLoading: boolean): void {
this.loading = isLoading;
this._changeRef.detectChanges();
}
ngOnInit() {
this.updatePreview();
}
/**
* Updates the preview of markdown component with latest changes
* If content is empty and in non-edit mode, default it to 'Double-click to edit'
* Sanitizes the data to be shown in markdown cell
*/
private updatePreview() {
if (!this._bundleOptions || !this._cellModel) {
return;
}
let trustedChanged = this._bundleOptions && this._lastTrustedMode !== this.isTrusted;
if (trustedChanged || !this._initialized) {
this._lastTrustedMode = this.isTrusted;
let content = this._bundleOptions.data['text/markdown'];
if (useInProcMarkdown(this._configurationService)) {
this._markdownRenderer.setNotebookURI(this.cellModel.notebookModel.notebookUri);
let markdownResult = this._markdownRenderer.render({
isTrusted: this.cellModel.trustedMode,
value: content.toString()
});
let outputElement = <HTMLElement>this.output.nativeElement;
outputElement.innerHTML = markdownResult.element.innerHTML;
} else {
if (!content) {
} else {
this._commandService.executeCommand<string>('notebook.showPreview', this._cellModel.notebookModel.notebookUri, content).then((htmlcontent) => {
htmlcontent = convertVscodeResourceToFileInSubDirectories(htmlcontent, this._cellModel);
htmlcontent = this.sanitizeContent(htmlcontent);
let outputElement = <HTMLElement>this.output.nativeElement;
outputElement.innerHTML = htmlcontent;
this.setLoading(false);
});
}
}
this._initialized = true;
}
}
//Sanitizes the content based on trusted mode of Cell Model
private sanitizeContent(content: string): string {
if (this.isTrusted) {
content = this.sanitizer.sanitize(content);
}
return content;
}
public layout() {
// Do we need to update on layout changed?
}
public handleContentChanged(): void {
this.updatePreview();
}
}

View File

@@ -0,0 +1,194 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Type } from '@angular/core';
import * as platform from 'vs/platform/registry/common/platform';
import { ReadonlyJSONObject } from 'sql/workbench/parts/notebook/common/models/jsonext';
import { MimeModel } from 'sql/workbench/parts/notebook/common/models/mimemodel';
import * as types from 'vs/base/common/types';
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
export type FactoryIdentifier = string;
export const Extensions = {
MimeComponentContribution: 'notebook.contributions.mimecomponents'
};
export interface IMimeComponent {
bundleOptions: MimeModel.IOptions;
mimeType: string;
cellModel?: ICellModel;
layout(): void;
}
export interface IMimeComponentDefinition {
/**
* Whether the component is a "safe" component.
*
* #### Notes
* A "safe" component produces renderer widgets which can render
* untrusted model data in a usable way. *All* renderers must
* handle untrusted data safely, but some may simply failover
* with a "Run cell to view output" message. A "safe" renderer
* is an indication that its sanitized output will be useful.
*/
readonly safe: boolean;
/**
* The mime types handled by this component.
*/
readonly mimeTypes: ReadonlyArray<string>;
/**
* The angular selector for this component
*/
readonly selector: string;
/**
* The default rank of the factory. If not given, defaults to 100.
*/
readonly rank?: number;
readonly ctor: Type<IMimeComponent>;
}
export type SafetyLevel = 'ensure' | 'prefer' | 'any';
type RankPair = { readonly id: number; readonly rank: number };
/**
* A type alias for a mapping of mime type -> rank pair.
*/
type RankMap = { [key: string]: RankPair };
/**
* A type alias for a mapping of mime type -> ordered factories.
*/
export type ComponentMap = { [key: string]: IMimeComponentDefinition };
export interface IMimeComponentRegistry {
/**
* Add a MIME component to the registry.
*
* @param componentDefinition - The definition of this component including
* the constructor to initialize it, supported `mimeTypes`, and `rank` order
* of preference vs. other mime types.
* If no `rank` is given, it will default to 100.
*
* #### Notes
* The renderer will replace an existing renderer for the given
* mimeType.
*/
registerComponentType(componentDefinition: IMimeComponentDefinition): void;
/**
* Find the preferred mime type for a mime bundle.
*
* @param bundle - The bundle of mime data.
*
* @param safe - How to consider safe/unsafe factories. If 'ensure',
* it will only consider safe factories. If 'any', any factory will be
* considered. If 'prefer', unsafe factories will be considered, but
* only after the safe options have been exhausted.
*
* @returns The preferred mime type from the available factories,
* or `undefined` if the mime type cannot be rendered.
*/
getPreferredMimeType(bundle: ReadonlyJSONObject, safe: SafetyLevel): string;
getCtorFromMimeType(mimeType: string): Type<IMimeComponent>;
getAllCtors(): Array<Type<IMimeComponent>>;
getAllMimeTypes(): Array<string>;
}
class MimeComponentRegistry implements IMimeComponentRegistry {
private _id = 0;
private _ranks: RankMap = {};
private _types: string[] | null = null;
private _componentDefinitions: ComponentMap = {};
registerComponentType(componentDefinition: IMimeComponentDefinition): void {
let rank = !types.isUndefinedOrNull(componentDefinition.rank) ? componentDefinition.rank : 100;
for (let mt of componentDefinition.mimeTypes) {
this._componentDefinitions[mt] = componentDefinition;
this._ranks[mt] = { rank, id: this._id++ };
}
this._types = null;
}
public getPreferredMimeType(bundle: ReadonlyJSONObject, safe: SafetyLevel = 'ensure'): string | undefined {
// Try to find a safe factory first, if preferred.
if (safe === 'ensure' || safe === 'prefer') {
for (let mt of this.mimeTypes) {
if (mt in bundle && this._componentDefinitions[mt].safe) {
return mt;
}
}
}
if (safe !== 'ensure') {
// Otherwise, search for the best factory among all factories.
for (let mt of this.mimeTypes) {
if (mt in bundle) {
return mt;
}
}
}
// Otherwise, no matching mime type exists.
return undefined;
}
public getCtorFromMimeType(mimeType: string): Type<IMimeComponent> {
let componentDescriptor = this._componentDefinitions[mimeType];
return componentDescriptor ? componentDescriptor.ctor : undefined;
}
public getAllCtors(): Array<Type<IMimeComponent>> {
let addedCtors = [];
let ctors = Object.values(this._componentDefinitions)
.map((c: IMimeComponentDefinition) => c.ctor)
.filter(ctor => {
let shouldAdd = !addedCtors.find((ctor2) => ctor === ctor2);
if (shouldAdd) {
addedCtors.push(ctor);
}
return shouldAdd;
});
return ctors;
}
public getAllMimeTypes(): Array<string> {
return Object.keys(this._componentDefinitions);
}
/**
* The ordered list of mimeTypes.
*/
get mimeTypes(): ReadonlyArray<string> {
return this._types || (this._types = sortedTypes(this._ranks));
}
}
const componentRegistry = new MimeComponentRegistry();
platform.Registry.add(Extensions.MimeComponentContribution, componentRegistry);
export function registerComponentType(componentDefinition: IMimeComponentDefinition): void {
componentRegistry.registerComponentType(componentDefinition);
}
/**
* Get the mime types in the map, ordered by rank.
*/
function sortedTypes(map: RankMap): string[] {
return Object.keys(map).sort((a, b) => {
let p1 = map[a];
let p2 = map[b];
if (p1.rank !== p2.rank) {
return p1.rank - p2.rank;
}
return p1.id - p2.id;
});
}

View File

@@ -0,0 +1,78 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IMimeComponent } from 'sql/workbench/parts/notebook/browser/outputs/mimeRegistry';
import { AngularDisposable } from 'sql/base/browser/lifecycle';
import { ElementRef, forwardRef, Inject, Component, OnInit, Input } from '@angular/core';
import { MimeModel } from 'sql/workbench/parts/notebook/common/models/mimemodel';
import { INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
import { RenderMimeRegistry } from 'sql/workbench/parts/notebook/browser/outputs/registry';
import { localize } from 'vs/nls';
@Component({
selector: MimeRendererComponent.SELECTOR,
template: ``
})
export class MimeRendererComponent extends AngularDisposable implements IMimeComponent, OnInit {
public static readonly SELECTOR = 'mime-output';
private _bundleOptions: MimeModel.IOptions;
private registry: RenderMimeRegistry;
private _initialized: boolean = false;
constructor(
@Inject(forwardRef(() => ElementRef)) private el: ElementRef,
@Inject(INotebookService) private _notebookService: INotebookService,
) {
super();
this.registry = this._notebookService.getMimeRegistry();
}
@Input() set bundleOptions(value: MimeModel.IOptions) {
this._bundleOptions = value;
if (this._initialized) {
this.renderOutput();
}
}
@Input() mimeType: string;
ngOnInit(): void {
this.renderOutput();
this._initialized = true;
}
layout(): void {
// Re-layout the output when layout is requested
this.renderOutput();
}
private renderOutput(): void {
// TODO handle safe/unsafe mapping
this.createRenderedMimetype(this._bundleOptions, this.el.nativeElement);
}
protected createRenderedMimetype(options: MimeModel.IOptions, node: HTMLElement): void {
if (this.mimeType) {
let renderer = this.registry.createRenderer(this.mimeType);
renderer.node = node;
let model = new MimeModel(options);
renderer.renderModel(model).catch(error => {
// Manually append error message to output
renderer.node.innerHTML = `<pre>Javascript Error: ${error.message}</pre>`;
// Remove mime-type-specific CSS classes
renderer.node.className = 'p-Widget jp-RenderedText';
renderer.node.setAttribute(
'data-mime-type',
'application/vnd.jupyter.stderr'
);
});
} else {
node.innerHTML = localize('noRendererFound',
"No {0} renderer could be found for output. It has the following MIME types: {1}",
options.trusted ? '' : localize('safe', "(safe) "),
Object.keys(options.data).join(', '));
}
}
}

View File

@@ -0,0 +1,239 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { URI } from 'vs/base/common/uri';
import { RenderOptions } from 'vs/base/browser/htmlContentRenderer';
import { IMarkdownString, removeMarkdownEscapes } from 'vs/base/common/htmlContent';
import { IMarkdownRenderResult } from 'vs/editor/contrib/markdown/markdownRenderer';
import marked = require('vs/base/common/marked/marked');
import { defaultGenerator } from 'vs/base/common/idGenerator';
import { revive } from 'vs/base/common/marshalling';
import * as fs from 'fs';
// Based off of HtmlContentRenderer
export class NotebookMarkdownRenderer {
private _notebookURI: URI;
private _baseUrls: string[] = [];
constructor() {
}
render(markdown: IMarkdownString): IMarkdownRenderResult {
const element: HTMLElement = markdown ? this.renderMarkdown(markdown, undefined) : document.createElement('span');
return {
element,
dispose: () => { }
};
}
createElement(options: RenderOptions): HTMLElement {
const tagName = options.inline ? 'span' : 'div';
const element = document.createElement(tagName);
if (options.className) {
element.className = options.className;
}
return element;
}
parse(text: string): any {
let data = JSON.parse(text);
data = revive(data, 0);
return data;
}
/**
* Create html nodes for the given content element.
* Adapted from htmlContentRenderer. Ensures that the markdown renderer
* gets passed in the correct baseUrl for the notebook's saved location,
* respects the trusted state of a notebook, and allows command links to
* be clickable.
*/
renderMarkdown(markdown: IMarkdownString, options: RenderOptions = {}): HTMLElement {
const element = this.createElement(options);
// signal to code-block render that the element has been created
let signalInnerHTML: () => void;
const withInnerHTML = new Promise(c => signalInnerHTML = c);
let notebookFolder = path.dirname(this._notebookURI.path) + '/';
if (!this._baseUrls.includes(notebookFolder)) {
this._baseUrls.push(notebookFolder);
}
const renderer = new marked.Renderer({ baseUrl: notebookFolder });
renderer.image = (href: string, title: string, text: string) => {
href = this.cleanUrl(!markdown.isTrusted, notebookFolder, href);
let dimensions: string[] = [];
if (href) {
const splitted = href.split('|').map(s => s.trim());
href = splitted[0];
const parameters = splitted[1];
if (parameters) {
const heightFromParams = /height=(\d+)/.exec(parameters);
const widthFromParams = /width=(\d+)/.exec(parameters);
const height = heightFromParams ? heightFromParams[1] : '';
const width = widthFromParams ? widthFromParams[1] : '';
const widthIsFinite = isFinite(parseInt(width));
const heightIsFinite = isFinite(parseInt(height));
if (widthIsFinite) {
dimensions.push(`width="${width}"`);
}
if (heightIsFinite) {
dimensions.push(`height="${height}"`);
}
}
}
let attributes: string[] = [];
if (href) {
attributes.push(`src="${href}"`);
}
if (text) {
attributes.push(`alt="${text}"`);
}
if (title) {
attributes.push(`title="${title}"`);
}
if (dimensions.length) {
attributes = attributes.concat(dimensions);
}
return '<img ' + attributes.join(' ') + '>';
};
renderer.link = (href: string, title: string, text: string): string => {
href = this.cleanUrl(!markdown.isTrusted, notebookFolder, href);
if (href === null) {
return text;
}
// Remove markdown escapes. Workaround for https://github.com/chjj/marked/issues/829
if (href === text) { // raw link case
text = removeMarkdownEscapes(text);
}
title = removeMarkdownEscapes(title);
href = removeMarkdownEscapes(href);
if (
!href
|| !markdown.isTrusted
|| href.match(/^data:|javascript:/i)
|| href.match(/^command:(\/\/\/)?_workbench\.downloadResource/i)
) {
// drop the link
return text;
} else {
// HTML Encode href
href = href.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
return `<a href=${href} data-href="${href}" title="${title || href}">${text}</a>`;
}
};
renderer.paragraph = (text): string => {
return `<p>${text}</p>`;
};
if (options.codeBlockRenderer) {
renderer.code = (code, lang) => {
const value = options.codeBlockRenderer!(lang, code);
// when code-block rendering is async we return sync
// but update the node with the real result later.
const id = defaultGenerator.nextId();
const promise = value.then(strValue => {
withInnerHTML.then(e => {
const span = element.querySelector(`div[data-code="${id}"]`);
if (span) {
span.innerHTML = strValue;
}
}).catch(err => {
// ignore
});
});
if (options.codeBlockRenderCallback) {
promise.then(options.codeBlockRenderCallback);
}
return `<div class="code" data-code="${id}">${escape(code)}</div>`;
};
}
const markedOptions: marked.MarkedOptions = {
sanitize: !markdown.isTrusted,
renderer,
baseUrl: notebookFolder
};
element.innerHTML = marked.parse(markdown.value, markedOptions);
signalInnerHTML!();
return element;
}
// This following methods have been adapted from marked.js
// Copyright (c) 2011-2014, Christopher Jeffrey (https://github.com/chjj/)
cleanUrl(sanitize: boolean, base: string, href: string) {
if (sanitize) {
let prot: string;
try {
prot = decodeURIComponent(unescape(href))
.replace(/[^\w:]/g, '')
.toLowerCase();
} catch (e) {
return null;
}
if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
return null;
}
}
try {
if (URI.parse(href)) {
return href;
}
} catch {
// ignore
}
let originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;
if (base && !originIndependentUrl.test(href) && !fs.existsSync(href)) {
href = this.resolveUrl(base, href);
}
try {
href = encodeURI(href).replace(/%25/g, '%');
} catch (e) {
return null;
}
return href;
}
resolveUrl(base: string, href: string) {
if (!this._baseUrls[' ' + base]) {
// we can ignore everything in base after the last slash of its path component,
// but we might need to add _that_
// https://tools.ietf.org/html/rfc3986#section-3
if (/^[^:]+:\/*[^/]*$/.test(base)) {
this._baseUrls[' ' + base] = base + '/';
} else {
// Remove trailing 'c's. /c*$/ is vulnerable to REDOS.
this._baseUrls[' ' + base] = base.replace(/c*$/, '');
}
}
base = this._baseUrls[' ' + base];
if (href.slice(0, 2) === '//') {
return base.replace(/:[\s\S]*/, ':') + href;
} else if (href.charAt(0) === '/') {
return base.replace(/(:\/*[^/]*)[\s\S]*/, '$1') + href;
} else {
return base + href;
}
}
// end marked.js adaptation
setNotebookURI(val: URI) {
this._notebookURI = val;
}
}

View File

@@ -0,0 +1,157 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { OnInit, Component, Input, Inject, ElementRef, ViewChild } from '@angular/core';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { localize } from 'vs/nls';
import * as types from 'vs/base/common/types';
import { AngularDisposable } from 'sql/base/browser/lifecycle';
import { IMimeComponent } from 'sql/workbench/parts/notebook/browser/outputs/mimeRegistry';
import { ICellModel } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
import { MimeModel } from 'sql/workbench/parts/notebook/common/models/mimemodel';
import { getErrorMessage } from 'vs/base/common/errors';
type ObjectType = object;
interface FigureLayout extends ObjectType {
width?: string | number;
height?: string;
autosize?: boolean;
}
interface Figure extends ObjectType {
data: object[];
layout: Partial<FigureLayout>;
}
declare class PlotlyHTMLElement extends HTMLDivElement {
data: object;
layout: object;
newPlot: () => void;
redraw: () => void;
}
@Component({
selector: PlotlyOutputComponent.SELECTOR,
template: `<div #output class="plotly-wrapper"></div>
<pre *ngIf="hasError" class="p-Widget jp-RenderedText">{{errorText}}</pre>
`
})
export class PlotlyOutputComponent extends AngularDisposable implements IMimeComponent, OnInit {
public static readonly SELECTOR: string = 'plotly-output';
Plotly!: {
newPlot: (
div: PlotlyHTMLElement | null | undefined,
data: object,
layout: FigureLayout
) => void;
redraw: (div?: PlotlyHTMLElement) => void;
};
@ViewChild('output', { read: ElementRef }) private output: ElementRef;
private _initialized: boolean = false;
private _rendered: boolean = false;
private _cellModel: ICellModel;
private _bundleOptions: MimeModel.IOptions;
private _plotDiv: PlotlyHTMLElement;
public errorText: string;
constructor(
@Inject(IThemeService) private readonly themeService: IThemeService
) {
super();
}
@Input() set bundleOptions(value: MimeModel.IOptions) {
this._bundleOptions = value;
if (this._initialized) {
this.renderPlotly();
}
}
@Input() mimeType: string;
get cellModel(): ICellModel {
return this._cellModel;
}
@Input() set cellModel(value: ICellModel) {
this._cellModel = value;
if (this._initialized) {
this.renderPlotly();
}
}
ngOnInit() {
this.Plotly = require.__$__nodeRequire('plotly.js-dist');
this._plotDiv = this.output.nativeElement;
this.renderPlotly();
this._initialized = true;
}
renderPlotly(): void {
if (this._rendered) {
// just re-layout
this.layout();
return;
}
if (!this._bundleOptions || !this._cellModel || !this.mimeType) {
return;
}
if (this.mimeType === 'text/vnd.plotly.v1+html') {
// Do nothing - this is our way to ignore the offline init Plotly attempts to do via a <script> tag.
// We have "handled" it by pulling in the plotly library into this component instead
return;
}
this.errorText = undefined;
const figure = this.getFigure(true);
if (figure) {
figure.layout = figure.layout || {};
if (!figure.layout.width && !figure.layout.autosize) {
// Workaround: to avoid filling up the entire cell, use plotly's default
figure.layout.width = Math.min(700, this._plotDiv.clientWidth);
}
try {
this.Plotly.newPlot(this._plotDiv, figure.data, figure.layout);
} catch (error) {
this.displayError(error);
}
}
this._rendered = true;
}
getFigure(showError: boolean): Figure {
const figure = <Figure><any>this._bundleOptions.data[this.mimeType];
if (typeof figure === 'string') {
try {
JSON.parse(figure);
} catch (error) {
if (showError) {
this.displayError(error);
}
}
}
const { data = [], layout = {} } = figure;
return { data, layout };
}
private displayError(error: Error | string): void {
this.errorText = localize('plotlyError', 'Error displaying Plotly graph: {0}', getErrorMessage(error));
}
layout(): void {
// No need to re-layout for now as Plotly is doing its own resize handling.
}
public hasError(): boolean {
return !types.isUndefinedOrNull(this.errorText);
}
}

View File

@@ -0,0 +1,352 @@
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
import { IRenderMime } from '../../common/models/renderMimeInterfaces';
import { MimeModel } from '../../common/models/mimemodel';
import { ReadonlyJSONObject } from '../../common/models/jsonext';
import { defaultSanitizer } from './sanitizer';
/**
* An object which manages mime renderer factories.
*
* This object is used to render mime models using registered mime
* renderers, selecting the preferred mime renderer to render the
* model into a widget.
*
* #### Notes
* This class is not intended to be subclassed.
*/
export class RenderMimeRegistry {
/**
* Construct a new rendermime.
*
* @param options - The options for initializing the instance.
*/
constructor(options: RenderMimeRegistry.IOptions = {}) {
// Parse the options.
this.resolver = options.resolver || null;
this.linkHandler = options.linkHandler || null;
this.latexTypesetter = options.latexTypesetter || null;
this.sanitizer = options.sanitizer || defaultSanitizer;
// Add the initial factories.
if (options.initialFactories) {
for (let factory of options.initialFactories) {
this.addFactory(factory);
}
}
}
/**
* The sanitizer used by the rendermime instance.
*/
readonly sanitizer: IRenderMime.ISanitizer;
/**
* The object used to resolve relative urls for the rendermime instance.
*/
readonly resolver: IRenderMime.IResolver | null;
/**
* The object used to handle path opening links.
*/
readonly linkHandler: IRenderMime.ILinkHandler | null;
/**
* The LaTeX typesetter for the rendermime.
*/
readonly latexTypesetter: IRenderMime.ILatexTypesetter | null;
/**
* The ordered list of mimeTypes.
*/
get mimeTypes(): ReadonlyArray<string> {
return this._types || (this._types = Private.sortedTypes(this._ranks));
}
/**
* Find the preferred mime type for a mime bundle.
*
* @param bundle - The bundle of mime data.
*
* @param safe - How to consider safe/unsafe factories. If 'ensure',
* it will only consider safe factories. If 'any', any factory will be
* considered. If 'prefer', unsafe factories will be considered, but
* only after the safe options have been exhausted.
*
* @returns The preferred mime type from the available factories,
* or `undefined` if the mime type cannot be rendered.
*/
preferredMimeType(
bundle: ReadonlyJSONObject,
safe: 'ensure' | 'prefer' | 'any' = 'ensure'
): string | undefined {
// Try to find a safe factory first, if preferred.
if (safe === 'ensure' || safe === 'prefer') {
for (let mt of this.mimeTypes) {
if (mt in bundle && this._factories[mt].safe) {
return mt;
}
}
}
if (safe !== 'ensure') {
// Otherwise, search for the best factory among all factories.
for (let mt of this.mimeTypes) {
if (mt in bundle) {
return mt;
}
}
}
// Otherwise, no matching mime type exists.
return undefined;
}
/**
* Create a renderer for a mime type.
*
* @param mimeType - The mime type of interest.
*
* @returns A new renderer for the given mime type.
*
* @throws An error if no factory exists for the mime type.
*/
createRenderer(mimeType: string): IRenderMime.IRenderer {
// Throw an error if no factory exists for the mime type.
if (!(mimeType in this._factories)) {
throw new Error(`No factory for mime type: '${mimeType}'`);
}
// Invoke the best factory for the given mime type.
return this._factories[mimeType].createRenderer({
mimeType,
resolver: this.resolver,
sanitizer: this.sanitizer,
linkHandler: this.linkHandler,
latexTypesetter: this.latexTypesetter
});
}
/**
* Create a new mime model. This is a convenience method.
*
* @options - The options used to create the model.
*
* @returns A new mime model.
*/
createModel(options: MimeModel.IOptions = {}): MimeModel {
return new MimeModel(options);
}
/**
* Create a clone of this rendermime instance.
*
* @param options - The options for configuring the clone.
*
* @returns A new independent clone of the rendermime.
*/
clone(options: RenderMimeRegistry.ICloneOptions = {}): RenderMimeRegistry {
// Create the clone.
let clone = new RenderMimeRegistry({
resolver: options.resolver || this.resolver || undefined,
sanitizer: options.sanitizer || this.sanitizer || undefined,
linkHandler: options.linkHandler || this.linkHandler || undefined,
latexTypesetter: options.latexTypesetter || this.latexTypesetter
});
// Clone the internal state.
clone._factories = { ...this._factories };
clone._ranks = { ...this._ranks };
clone._id = this._id;
// Return the cloned object.
return clone;
}
/**
* Get the renderer factory registered for a mime type.
*
* @param mimeType - The mime type of interest.
*
* @returns The factory for the mime type, or `undefined`.
*/
getFactory(mimeType: string): IRenderMime.IRendererFactory | undefined {
return this._factories[mimeType];
}
/**
* Add a renderer factory to the rendermime.
*
* @param factory - The renderer factory of interest.
*
* @param rank - The rank of the renderer. A lower rank indicates
* a higher priority for rendering. If not given, the rank will
* defer to the `defaultRank` of the factory. If no `defaultRank`
* is given, it will default to 100.
*
* #### Notes
* The renderer will replace an existing renderer for the given
* mimeType.
*/
addFactory(factory: IRenderMime.IRendererFactory, rank?: number): void {
if (rank === undefined) {
rank = factory.defaultRank;
if (rank === undefined) {
rank = 100;
}
}
for (let mt of factory.mimeTypes) {
this._factories[mt] = factory;
this._ranks[mt] = { rank, id: this._id++ };
}
this._types = null;
}
/**
* Remove a mime type.
*
* @param mimeType - The mime type of interest.
*/
removeMimeType(mimeType: string): void {
delete this._factories[mimeType];
delete this._ranks[mimeType];
this._types = null;
}
/**
* Get the rank for a given mime type.
*
* @param mimeType - The mime type of interest.
*
* @returns The rank of the mime type or undefined.
*/
getRank(mimeType: string): number | undefined {
let rank = this._ranks[mimeType];
return rank && rank.rank;
}
/**
* Set the rank of a given mime type.
*
* @param mimeType - The mime type of interest.
*
* @param rank - The new rank to assign.
*
* #### Notes
* This is a no-op if the mime type is not registered.
*/
setRank(mimeType: string, rank: number): void {
if (!this._ranks[mimeType]) {
return;
}
let id = this._id++;
this._ranks[mimeType] = { rank, id };
this._types = null;
}
private _id = 0;
private _ranks: Private.RankMap = {};
private _types: string[] | null = null;
private _factories: Private.FactoryMap = {};
}
/**
* The namespace for `RenderMimeRegistry` class statics.
*/
export namespace RenderMimeRegistry {
/**
* The options used to initialize a rendermime instance.
*/
export interface IOptions {
/**
* Initial factories to add to the rendermime instance.
*/
initialFactories?: ReadonlyArray<IRenderMime.IRendererFactory>;
/**
* The sanitizer used to sanitize untrusted html inputs.
*
* If not given, a default sanitizer will be used.
*/
sanitizer?: IRenderMime.ISanitizer;
/**
* The initial resolver object.
*
* The default is `null`.
*/
resolver?: IRenderMime.IResolver;
/**
* An optional path handler.
*/
linkHandler?: IRenderMime.ILinkHandler;
/**
* An optional LaTeX typesetter.
*/
latexTypesetter?: IRenderMime.ILatexTypesetter;
}
/**
* The options used to clone a rendermime instance.
*/
export interface ICloneOptions {
/**
* The new sanitizer used to sanitize untrusted html inputs.
*/
sanitizer?: IRenderMime.ISanitizer;
/**
* The new resolver object.
*/
resolver?: IRenderMime.IResolver;
/**
* The new path handler.
*/
linkHandler?: IRenderMime.ILinkHandler;
/**
* The new LaTeX typesetter.
*/
latexTypesetter?: IRenderMime.ILatexTypesetter;
}
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* A type alias for a mime rank and tie-breaking id.
*/
export type RankPair = { readonly id: number; readonly rank: number };
/**
* A type alias for a mapping of mime type -> rank pair.
*/
export type RankMap = { [key: string]: RankPair };
/**
* A type alias for a mapping of mime type -> ordered factories.
*/
export type FactoryMap = { [key: string]: IRenderMime.IRendererFactory };
/**
* Get the mime types in the map, ordered by rank.
*/
export function sortedTypes(map: RankMap): string[] {
return Object.keys(map).sort((a, b) => {
let p1 = map[a];
let p2 = map[b];
if (p1.rank !== p2.rank) {
return p1.rank - p2.rank;
}
return p1.id - p2.id;
});
}
}

View File

@@ -0,0 +1,649 @@
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
import { default as AnsiUp } from 'ansi_up';
import { IRenderMime } from '../../common/models/renderMimeInterfaces';
import { URLExt } from '../../common/models/url';
import { URI } from 'vs/base/common/uri';
/**
* Render HTML into a host node.
*
* @params options - The options for rendering.
*
* @returns A promise which resolves when rendering is complete.
*/
export function renderHTML(options: renderHTML.IOptions): Promise<void> {
// Unpack the options.
let {
host,
source,
trusted,
sanitizer,
resolver,
linkHandler,
shouldTypeset,
latexTypesetter
} = options;
let originalSource = source;
// Bail early if the source is empty.
if (!source) {
host.textContent = '';
return Promise.resolve(undefined);
}
// Sanitize the source if it is not trusted. This removes all
// `<script>` tags as well as other potentially harmful HTML.
if (!trusted) {
originalSource = `${source}`;
source = sanitizer.sanitize(source);
}
// Set the inner HTML of the host.
host.innerHTML = source;
if (host.getElementsByTagName('script').length > 0) {
// If output it trusted, eval any script tags contained in the HTML.
// This is not done automatically by the browser when script tags are
// created by setting `innerHTML`.
if (trusted) {
Private.evalInnerHTMLScriptTags(host);
} else {
const container = document.createElement('div');
const warning = document.createElement('pre');
warning.textContent =
'This HTML output contains inline scripts. Are you sure that you want to run arbitrary Javascript within your Notebook session?';
const runButton = document.createElement('button');
runButton.textContent = 'Run';
runButton.onclick = event => {
host.innerHTML = originalSource;
Private.evalInnerHTMLScriptTags(host);
host.removeChild(host.firstChild);
};
container.appendChild(warning);
container.appendChild(runButton);
host.insertBefore(container, host.firstChild);
}
}
// Handle default behavior of nodes.
Private.handleDefaults(host, resolver);
// Patch the urls if a resolver is available.
let promise: Promise<void>;
if (resolver) {
promise = Private.handleUrls(host, resolver, linkHandler);
} else {
promise = Promise.resolve(undefined);
}
// Return the final rendered promise.
return promise.then(() => {
if (shouldTypeset && latexTypesetter) {
latexTypesetter.typeset(host);
}
});
}
/**
* The namespace for the `renderHTML` function statics.
*/
export namespace renderHTML {
/**
* The options for the `renderHTML` function.
*/
export interface IOptions {
/**
* The host node for the rendered HTML.
*/
host: HTMLElement;
/**
* The HTML source to render.
*/
source: string;
/**
* Whether the source is trusted.
*/
trusted: boolean;
/**
* The html sanitizer for untrusted source.
*/
sanitizer: IRenderMime.ISanitizer;
/**
* An optional url resolver.
*/
resolver: IRenderMime.IResolver | null;
/**
* An optional link handler.
*/
linkHandler: IRenderMime.ILinkHandler | null;
/**
* Whether the node should be typeset.
*/
shouldTypeset: boolean;
/**
* The LaTeX typesetter for the application.
*/
latexTypesetter: IRenderMime.ILatexTypesetter | null;
}
}
/**
* Render an image into a host node.
*
* @params options - The options for rendering.
*
* @returns A promise which resolves when rendering is complete.
*/
export function renderImage(
options: renderImage.IRenderOptions
): Promise<void> {
// Unpack the options.
let {
host,
mimeType,
source,
width,
height,
needsBackground,
unconfined
} = options;
// Clear the content in the host.
host.textContent = '';
// Create the image element.
let img = document.createElement('img');
// Set the source of the image.
img.src = `data:${mimeType};base64,${source}`;
// Set the size of the image if provided.
if (typeof height === 'number') {
img.height = height;
}
if (typeof width === 'number') {
img.width = width;
}
if (needsBackground === 'light') {
img.classList.add('jp-needs-light-background');
} else if (needsBackground === 'dark') {
img.classList.add('jp-needs-dark-background');
}
if (unconfined === true) {
img.classList.add('jp-mod-unconfined');
}
// Add the image to the host.
host.appendChild(img);
// Return the rendered promise.
return Promise.resolve(undefined);
}
/**
* The namespace for the `renderImage` function statics.
*/
export namespace renderImage {
/**
* The options for the `renderImage` function.
*/
export interface IRenderOptions {
/**
* The image node to update with the content.
*/
host: HTMLElement;
/**
* The mime type for the image.
*/
mimeType: string;
/**
* The base64 encoded source for the image.
*/
source: string;
/**
* The optional width for the image.
*/
width?: number;
/**
* The optional height for the image.
*/
height?: number;
/**
* Whether an image requires a background for legibility.
*/
needsBackground?: string;
/**
* Whether the image should be unconfined.
*/
unconfined?: boolean;
}
}
/**
* Render LaTeX into a host node.
*
* @params options - The options for rendering.
*
* @returns A promise which resolves when rendering is complete.
*/
export function renderLatex(
options: renderLatex.IRenderOptions
): Promise<void> {
// Unpack the options.
let { host, source, shouldTypeset, latexTypesetter } = options;
// Set the source on the node.
host.textContent = source;
// Typeset the node if needed.
if (shouldTypeset && latexTypesetter) {
latexTypesetter.typeset(host);
}
// Return the rendered promise.
return Promise.resolve(undefined);
}
/**
* The namespace for the `renderLatex` function statics.
*/
export namespace renderLatex {
/**
* The options for the `renderLatex` function.
*/
export interface IRenderOptions {
/**
* The host node for the rendered LaTeX.
*/
host: HTMLElement;
/**
* The LaTeX source to render.
*/
source: string;
/**
* Whether the node should be typeset.
*/
shouldTypeset: boolean;
/**
* The LaTeX typesetter for the application.
*/
latexTypesetter: IRenderMime.ILatexTypesetter | null;
}
}
/**
* The namespace for the `renderDataResource` function statics.
*/
export namespace renderDataResource {
/**
* The options for the `renderDataResource` function.
*/
export interface IRenderOptions {
/**
* The host node for the rendered LaTeX.
*/
host: HTMLElement;
/**
* The DataResource source to render.
*/
source: string;
}
}
/**
* Render SVG into a host node.
*
* @params options - The options for rendering.
*
* @returns A promise which resolves when rendering is complete.
*/
export function renderSVG(options: renderSVG.IRenderOptions): Promise<void> {
// Unpack the options.
let { host, source, trusted, unconfined } = options;
// Clear the content if there is no source.
if (!source) {
host.textContent = '';
return Promise.resolve(undefined);
}
// Display a message if the source is not trusted.
if (!trusted) {
host.textContent =
'Cannot display an untrusted SVG. Maybe you need to run the cell?';
return Promise.resolve(undefined);
}
// Render in img so that user can save it easily
const img = new Image();
img.src = `data:image/svg+xml,${encodeURIComponent(source)}`;
host.appendChild(img);
if (unconfined === true) {
host.classList.add('jp-mod-unconfined');
}
return Promise.resolve();
}
/**
* The namespace for the `renderSVG` function statics.
*/
export namespace renderSVG {
/**
* The options for the `renderSVG` function.
*/
export interface IRenderOptions {
/**
* The host node for the rendered SVG.
*/
host: HTMLElement;
/**
* The SVG source.
*/
source: string;
/**
* Whether the source is trusted.
*/
trusted: boolean;
/**
* Whether the svg should be unconfined.
*/
unconfined?: boolean;
}
}
/**
* Render text into a host node.
*
* @params options - The options for rendering.
*
* @returns A promise which resolves when rendering is complete.
*/
export function renderText(options: renderText.IRenderOptions): Promise<void> {
// Unpack the options.
let { host, source } = options;
const ansiUp = new AnsiUp();
ansiUp.escape_for_html = true;
ansiUp.use_classes = true;
// Create the HTML content.
let content = ansiUp.ansi_to_html(source);
// Set the inner HTML for the host node.
host.innerHTML = `<pre>${content}</pre>`;
// Return the rendered promise.
return Promise.resolve(undefined);
}
/**
* The namespace for the `renderText` function statics.
*/
export namespace renderText {
/**
* The options for the `renderText` function.
*/
export interface IRenderOptions {
/**
* The host node for the text content.
*/
host: HTMLElement;
/**
* The source text to render.
*/
source: string;
}
}
/**
* The namespace for module implementation details.
*/
namespace Private {
/**
* Eval the script tags contained in a host populated by `innerHTML`.
*
* When script tags are created via `innerHTML`, the browser does not
* evaluate them when they are added to the page. This function works
* around that by creating new equivalent script nodes manually, and
* replacing the originals.
*/
export function evalInnerHTMLScriptTags(host: HTMLElement): void {
// Create a snapshot of the current script nodes.
let scripts = host.getElementsByTagName('script');
// Loop over each script node.
for (let i = 0; i < scripts.length; i++) {
let script = scripts.item(i);
// Skip any scripts which no longer have a parent.
if (!script.parentNode) {
continue;
}
// Create a new script node which will be clone.
let clone = document.createElement('script');
// Copy the attributes into the clone.
let attrs = script.attributes;
for (let i = 0, n = attrs.length; i < n; ++i) {
let { name, value } = attrs[i];
clone.setAttribute(name, value);
}
// Copy the text content into the clone.
clone.textContent = script.textContent;
// Replace the old script in the parent.
script.parentNode.replaceChild(clone, script);
}
}
/**
* Handle the default behavior of nodes.
*/
export function handleDefaults(
node: HTMLElement,
resolver?: IRenderMime.IResolver
): void {
// Handle anchor elements.
let anchors = node.getElementsByTagName('a');
for (let i = 0; i < anchors.length; i++) {
let path = anchors[i].href || '';
const isLocal =
resolver && resolver.isLocal
? resolver.isLocal(path)
: URLExt.isLocal(path);
if (isLocal) {
anchors[i].target = '_self';
} else {
anchors[i].target = '_blank';
}
}
// Handle image elements.
let imgs = node.getElementsByTagName('img');
for (let i = 0; i < imgs.length; i++) {
if (!imgs[i].alt) {
imgs[i].alt = 'Image';
}
}
}
/**
* Resolve the relative urls in element `src` and `href` attributes.
*
* @param node - The head html element.
*
* @param resolver - A url resolver.
*
* @param linkHandler - An optional link handler for nodes.
*
* @returns a promise fulfilled when the relative urls have been resolved.
*/
export function handleUrls(
node: HTMLElement,
resolver: IRenderMime.IResolver,
linkHandler: IRenderMime.ILinkHandler | null
): Promise<void> {
// Set up an array to collect promises.
let promises: Promise<void>[] = [];
// Handle HTML Elements with src attributes.
let nodes = node.querySelectorAll('*[src]');
for (let i = 0; i < nodes.length; i++) {
promises.push(handleAttr(nodes[i] as HTMLElement, 'src', resolver));
}
// Handle anchor elements.
let anchors = node.getElementsByTagName('a');
for (let i = 0; i < anchors.length; i++) {
promises.push(handleAnchor(anchors[i], resolver, linkHandler));
}
// Handle link elements.
let links = node.getElementsByTagName('link');
for (let i = 0; i < links.length; i++) {
promises.push(handleAttr(links[i], 'href', resolver));
}
// Wait on all promises.
return Promise.all(promises).then(() => undefined);
}
/**
* Apply ids to headers.
*/
export function headerAnchors(node: HTMLElement): void {
let headerNames = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
for (let headerType of headerNames) {
let headers = node.getElementsByTagName(headerType);
for (let i = 0; i < headers.length; i++) {
let header = headers[i];
header.id = encodeURIComponent(header.innerHTML.replace(/ /g, '-'));
let anchor = document.createElement('a');
anchor.target = '_self';
anchor.textContent = '¶';
anchor.href = '#' + header.id;
anchor.classList.add('jp-InternalAnchorLink');
header.appendChild(anchor);
}
}
}
/**
* Handle a node with a `src` or `href` attribute.
*/
function handleAttr(
node: HTMLElement,
name: 'src' | 'href',
resolver: IRenderMime.IResolver
): Promise<void> {
let source = node.getAttribute(name) || '';
const isLocal = resolver.isLocal
? resolver.isLocal(source)
: URLExt.isLocal(source);
if (!source || !isLocal) {
return Promise.resolve(undefined);
}
node.setAttribute(name, '');
return resolver
.resolveUrl(source)
.then(path => {
return resolver.getDownloadUrl(path);
})
.then(url => {
// Check protocol again in case it changed:
if (URI.parse(url).scheme !== 'data:') {
// Bust caching for local src attrs.
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
url += (/\?/.test(url) ? '&' : '?') + new Date().getTime();
}
node.setAttribute(name, url);
})
.catch(err => {
// If there was an error getting the url,
// just make it an empty link.
node.setAttribute(name, '');
});
}
/**
* Handle an anchor node.
*/
function handleAnchor(
anchor: HTMLAnchorElement,
resolver: IRenderMime.IResolver,
linkHandler: IRenderMime.ILinkHandler | null
): Promise<void> {
// Get the link path without the location prepended.
// (e.g. "./foo.md#Header 1" vs "http://localhost:8888/foo.md#Header 1")
let href = anchor.getAttribute('href') || '';
const isLocal = resolver.isLocal
? resolver.isLocal(href)
: URLExt.isLocal(href);
// Bail if it is not a file-like url.
if (!href || !isLocal) {
return Promise.resolve(undefined);
}
// Remove the hash until we can handle it.
let hash = anchor.hash;
if (hash) {
// Handle internal link in the file.
if (hash === href) {
anchor.target = '_self';
return Promise.resolve(undefined);
}
// For external links, remove the hash until we have hash handling.
href = href.replace(hash, '');
}
// Get the appropriate file path.
return resolver
.resolveUrl(href)
.then(path => {
// Handle the click override.
if (linkHandler) {
linkHandler.handleLink(anchor, path, hash);
}
// Get the appropriate file download path.
return resolver.getDownloadUrl(path);
})
.then(url => {
// Set the visible anchor.
anchor.href = url + hash;
})
.catch(err => {
// If there was an error getting the url,
// just make it an empty link.
anchor.href = '';
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,378 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as renderers from './renderers';
import { IRenderMime } from '../../common/models/renderMimeInterfaces';
import { ReadonlyJSONObject } from '../../common/models/jsonext';
import * as tableRenderers from 'sql/workbench/parts/notebook/browser/outputs/tableRenderers';
/**
* A common base class for mime renderers.
*/
export abstract class RenderedCommon implements IRenderMime.IRenderer {
private _node: HTMLElement;
private cachedClasses: string[] = [];
/**
* Construct a new rendered common widget.
*
* @param options - The options for initializing the widget.
*/
constructor(options: IRenderMime.IRendererOptions) {
this.mimeType = options.mimeType;
this.sanitizer = options.sanitizer;
this.resolver = options.resolver;
this.linkHandler = options.linkHandler;
this.latexTypesetter = options.latexTypesetter;
}
public get node(): HTMLElement {
return this._node;
}
public set node(value: HTMLElement) {
this._node = value;
value.dataset['mimeType'] = this.mimeType;
this._node.classList.add(...this.cachedClasses);
this.cachedClasses = [];
}
toggleClass(className: string, enabled: boolean): void {
if (enabled) {
this.addClass(className);
} else {
this.removeClass(className);
}
}
addClass(className: string): void {
if (!this._node) {
this.cachedClasses.push(className);
} else if (!this._node.classList.contains(className)) {
this._node.classList.add(className);
}
}
removeClass(className: string): void {
if (!this._node) {
this.cachedClasses = this.cachedClasses.filter(c => c !== className);
} else if (this._node.classList.contains(className)) {
this._node.classList.remove(className);
}
}
/**
* The mimetype being rendered.
*/
readonly mimeType: string;
/**
* The sanitizer used to sanitize untrusted html inputs.
*/
readonly sanitizer: IRenderMime.ISanitizer;
/**
* The resolver object.
*/
readonly resolver: IRenderMime.IResolver | null;
/**
* The link handler.
*/
readonly linkHandler: IRenderMime.ILinkHandler | null;
/**
* The latexTypesetter.
*/
readonly latexTypesetter: IRenderMime.ILatexTypesetter;
/**
* Render a mime model.
*
* @param model - The mime model to render.
*
* @returns A promise which resolves when rendering is complete.
*/
renderModel(model: IRenderMime.IMimeModel): Promise<void> {
// TODO compare model against old model for early bail?
// Toggle the trusted class on the widget.
this.toggleClass('jp-mod-trusted', model.trusted);
// Render the actual content.
return this.render(model);
}
/**
* Render the mime model.
*
* @param model - The mime model to render.
*
* @returns A promise which resolves when rendering is complete.
*/
abstract render(model: IRenderMime.IMimeModel): Promise<void>;
}
/**
* A common base class for HTML mime renderers.
*/
export abstract class RenderedHTMLCommon extends RenderedCommon {
/**
* Construct a new rendered HTML common widget.
*
* @param options - The options for initializing the widget.
*/
constructor(options: IRenderMime.IRendererOptions) {
super(options);
this.addClass('jp-RenderedHTMLCommon');
}
}
/**
* A mime renderer for displaying HTML and math.
*/
export class RenderedHTML extends RenderedHTMLCommon {
/**
* Construct a new rendered HTML widget.
*
* @param options - The options for initializing the widget.
*/
constructor(options: IRenderMime.IRendererOptions) {
super(options);
this.addClass('jp-RenderedHTML');
}
/**
* Render a mime model.
*
* @param model - The mime model to render.
*
* @returns A promise which resolves when rendering is complete.
*/
render(model: IRenderMime.IMimeModel): Promise<void> {
return renderers.renderHTML({
host: this.node,
source: String(model.data[this.mimeType]),
trusted: model.trusted,
resolver: this.resolver,
sanitizer: this.sanitizer,
linkHandler: this.linkHandler,
shouldTypeset: true, //this.isAttached,
latexTypesetter: this.latexTypesetter
});
}
// /**
// * A message handler invoked on an `'after-attach'` message.
// */
// onAfterAttach(msg: Message): void {
// if (this.latexTypesetter) {
// this.latexTypesetter.typeset(this.node);
// }
// }
}
// /**
// * A mime renderer for displaying LaTeX output.
// */
// export class RenderedLatex extends RenderedCommon {
// /**
// * Construct a new rendered LaTeX widget.
// *
// * @param options - The options for initializing the widget.
// */
// constructor(options: IRenderMime.IRendererOptions) {
// super(options);
// this.addClass('jp-RenderedLatex');
// }
// /**
// * Render a mime model.
// *
// * @param model - The mime model to render.
// *
// * @returns A promise which resolves when rendering is complete.
// */
// render(model: IRenderMime.IMimeModel): Promise<void> {
// return renderers.renderLatex({
// host: this.node,
// source: String(model.data[this.mimeType]),
// shouldTypeset: this.isAttached,
// latexTypesetter: this.latexTypesetter
// });
// }
// /**
// * A message handler invoked on an `'after-attach'` message.
// */
// onAfterAttach(msg: Message): void {
// if (this.latexTypesetter) {
// this.latexTypesetter.typeset(this.node);
// }
// }
// }
/**
* A mime renderer for displaying images.
*/
export class RenderedImage extends RenderedCommon {
/**
* Construct a new rendered image widget.
*
* @param options - The options for initializing the widget.
*/
constructor(options: IRenderMime.IRendererOptions) {
super(options);
this.addClass('jp-RenderedImage');
}
/**
* Render a mime model.
*
* @param model - The mime model to render.
*
* @returns A promise which resolves when rendering is complete.
*/
render(model: IRenderMime.IMimeModel): Promise<void> {
let metadata = model.metadata[this.mimeType] as ReadonlyJSONObject;
return renderers.renderImage({
host: this.node,
mimeType: this.mimeType,
source: String(model.data[this.mimeType]),
width: metadata && (metadata.width as number | undefined),
height: metadata && (metadata.height as number | undefined),
needsBackground: model.metadata['needs_background'] as string | undefined,
unconfined: metadata && (metadata.unconfined as boolean | undefined)
});
}
}
/**
* A widget for displaying SVG content.
*/
export class RenderedSVG extends RenderedCommon {
/**
* Construct a new rendered SVG widget.
*
* @param options - The options for initializing the widget.
*/
constructor(options: IRenderMime.IRendererOptions) {
super(options);
this.addClass('jp-RenderedSVG');
}
/**
* Render a mime model.
*
* @param model - The mime model to render.
*
* @returns A promise which resolves when rendering is complete.
*/
render(model: IRenderMime.IMimeModel): Promise<void> {
let metadata = model.metadata[this.mimeType] as ReadonlyJSONObject;
return renderers.renderSVG({
host: this.node,
source: String(model.data[this.mimeType]),
trusted: model.trusted,
unconfined: metadata && (metadata.unconfined as boolean | undefined)
});
}
// /**
// * A message handler invoked on an `'after-attach'` message.
// */
// onAfterAttach(msg: Message): void {
// if (this.latexTypesetter) {
// this.latexTypesetter.typeset(this.node);
// }
// }
}
/**
* A widget for displaying plain text and console text.
*/
export class RenderedText extends RenderedCommon {
/**
* Construct a new rendered text widget.
*
* @param options - The options for initializing the widget.
*/
constructor(options: IRenderMime.IRendererOptions) {
super(options);
this.addClass('jp-RenderedText');
}
/**
* Render a mime model.
*
* @param model - The mime model to render.
*
* @returns A promise which resolves when rendering is complete.
*/
render(model: IRenderMime.IMimeModel): Promise<void> {
return renderers.renderText({
host: this.node,
source: String(model.data[this.mimeType])
});
}
}
/**
* A widget for displaying deprecated JavaScript output.
*/
export class RenderedJavaScript extends RenderedCommon {
/**
* Construct a new rendered text widget.
*
* @param options - The options for initializing the widget.
*/
constructor(options: IRenderMime.IRendererOptions) {
super(options);
this.addClass('jp-RenderedJavaScript');
}
/**
* Render a mime model.
*
* @param model - The mime model to render.
*
* @returns A promise which resolves when rendering is complete.
*/
render(model: IRenderMime.IMimeModel): Promise<void> {
return renderers.renderText({
host: this.node,
source: 'JavaScript output is disabled in Notebooks'
});
}
}
/**
* A widget for displaying Data Resource schemas and data.
*/
export class RenderedDataResource extends RenderedCommon {
/**
* Construct a new rendered data resource widget.
*
* @param options - The options for initializing the widget.
*/
constructor(options: IRenderMime.IRendererOptions) {
super(options);
this.addClass('jp-RenderedDataResource');
}
/**
* Render a mime model.
*
* @param model - The mime model to render.
*
* @returns A promise which resolves when rendering is complete.
*/
render(model: IRenderMime.IMimeModel): Promise<void> {
return tableRenderers.renderDataResource({
host: this.node,
source: JSON.stringify(model.data[this.mimeType]),
themeService: model.themeService
});
}
}