Compare commits

..

28 Commits
1.3.6 ... 1.3.7

Author SHA1 Message Date
Karl Burtram
5a30878599 Bump agent, import, profiler extension versions 2018-12-06 18:11:11 -08:00
Karl Burtram
c8a8935db0 Bump SQL Tools Service to 1.5.0-alpha.63 2018-12-06 17:58:49 -08:00
Aditya Bist
ec196f57bb Agent feature - ability to start at step (#3483)
* added ability to start at certain step

* fixed width for scrollbar

* localized string
2018-12-06 17:43:26 -08:00
Matt Irvine
f7809ec3a7 Add ability to select AAD tenant when connecting (#3475) 2018-12-06 10:46:27 -08:00
Kevin Cunnane
71d3ec3616 Fix #3420 Analyze in notebook doesn't include text (#3482)
- Add edit API that can be used in the extension
- Separated document and editor classes out since this is the point those get big. I can refactor back in if needed to ease code review
- Based this off text editing APIs but tweaked for the fact this is a cell/array based set of edits
2018-12-06 10:33:32 -08:00
Yurong He
4a7cf8d870 Disable key binding for this release. (#3466) 2018-12-06 10:04:02 -08:00
Alan Ren
4bf8836c0a profiler extension bug fixes (#3490) 2018-12-06 09:58:33 -08:00
ranasaria
1ca36ee29c bumping to version 62 to toolsservice to pick up resutlstreaming backend fixes 2018-12-05 22:39:23 -08:00
Yurong He
3446ff88cf Fixed #3472 check SaveKernelInfo not null (#3473) 2018-12-05 14:21:14 -08:00
Chris LaFreniere
de5a91a13f Allow for Notebook Cell Unselection (#3460)
* Allow for cell unselection

* PR Feedback: use event.stopPropagation() when multiple events can fire

* Ensure markdown goes into Preview mode when  cell not selected
2018-12-05 13:22:56 -08:00
Karl Burtram
814cd73019 Dispose of query grid memory on tab switch (#3458) 2018-12-05 12:17:16 -08:00
Aditya Bist
c21611661b Agent feature usage metrics (#3346)
* agent feature usage metrics

* generalized feature telemetry via dialogs

* renamed eventName property to dialogName

* made dialogName an optional field
2018-12-05 11:01:46 -08:00
Yurong He
8f817ce689 Fixed Notebooks regression: invalid kernels aren't handled #3404 (#3447)
* Fixed the not supported kernel in saved notebook

* Resolve PR comment.
2018-12-05 10:54:37 -08:00
Aditya Bist
971b5111e7 Agent: Scrolling (#3427)
* change height calcs to proper tree height

* agent fixes

* fit step tree

* fix rendering issue

* fixed spinning wheel position

* added horizontal scrolling to steps tree

* removed typo
2018-12-05 10:16:39 -08:00
Chris LaFreniere
07069a64ae Tweak notebook cell box shadow/border-width (#3454) 2018-12-04 19:21:55 -08:00
Karl Burtram
6acea51f12 Bump SQL Tools to 1.5.0-alpha.61 2018-12-04 17:51:51 -08:00
Raj
7aa2dab307 Defaulting 'attach to' to localhost when no extension is installed #3419 (#3426) 2018-12-04 13:45:10 -08:00
Raj
3091be8f67 Set active editor with dirty value before and after save #3411 (#3417) 2018-12-04 13:21:13 -08:00
Kevin Cunnane
487531cc52 Fix build break due to rename of APIs prior to checkin of unit tests (#3418) 2018-12-04 10:42:32 -08:00
Yurong He
58bfcb4273 Fixed can't read indexof exception when no big data cluster connection and switch to PySpark3 (#3413) 2018-12-04 10:36:34 -08:00
Kevin Cunnane
8d8be27f22 Add basic notebook model tests (#3396)
- Ported from the extension
- Only adding tests that related to the internally implemented functionality, not to anything provider-specific.
2018-12-04 10:01:10 -08:00
Yurong He
27a978cba5 Fixed #3387 (#3401) 2018-12-04 09:41:31 -08:00
Chris LaFreniere
71b4e6afa4 Fix Notebook Code Cell Height when Lines Wrap (#3403)
* Fix Notebook Code Cell Height when Lines Wrap

* Addressing PR comments
2018-12-03 20:55:46 -08:00
Chris LaFreniere
e1f3b19c0c Ensure Selectboxes in Notebook Toolbar have Minimum Size (#3393)
* Setting minimum width on selectboxes in notebook toolbar

* Fix spacing
2018-12-03 20:31:08 -08:00
Kevin Cunnane
649c2aa5a6 Fix preview handling so the untitled notebooks are pinned by default (#3405) 2018-12-03 20:26:42 -08:00
Kevin Cunnane
cac8cc99e1 Notebook extensibility: Move New Notebook and configuration to an extension (#3382)
initial support for Notebook extensibility. Fixes #3148 , Fixes #3382.

## Design notes
The extensibility patterns are modeled after the VSCode Document and Editor APIs but need to be different since core editor concepts are different - for example Notebooks have cells, and cells have contents rather than editors which have text lines.

Most importantly, a lot of the code is based on the MainThreadDocumentsAndEditors class, with some related classes (the MainThreadDocuments, and MainThreadEditors) brought in too. Given our current limitations I felt moving to add 3 full sets of extension host API classes was overkill so am currently using one. Will see if we need to change this in the future based on what we add in the additional APIs

## Limitations
The current implementation is limited to visible editors, rather than all documents in the workspace. We are not following the `openDocument` -> `showDocument` pattern, but instead just supporting `showDocument` directly.

## Changes in this PR
- Renamed existing APIs to make clear that they were about notebook contents, not about notebook behavior
- Added new APIs for querying notebook documents and editors 
- Added new API for opening a notebook
- Moved `New Notebook` command to an extension, and added an `Open Notebook` command too
- Moved notebook feature flag to the extension

## Not covered in this PR
- Need to actually implement support for defining the provider and connection IDs for a notebook. this will be important to support New Notebook from a big data connection in Object Explorer
- Need to add APIs for adding cells, to support 
- Need to implement the metadata for getting full notebook contents. I've only implemented to key APIs needed to make this all work.
2018-12-03 18:50:44 -08:00
Raj
cb162b16f2 No kernel is shown when open a new notebook from command palette (#3374)
Fixes #3271. Ensure a provider is defined when opening through command palette
2018-12-03 18:12:25 -08:00
Karl Burtram
86e54ce145 Bump Azure Data Studio to 1.3.7 2018-12-03 17:43:12 -08:00
92 changed files with 3449 additions and 352 deletions

View File

@@ -2,7 +2,7 @@
"name": "agent",
"displayName": "SQL Server Agent",
"description": "Manage and troubleshoot SQL Server Agent jobs",
"version": "0.35.1",
"version": "0.35.2",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",

View File

@@ -45,6 +45,7 @@ export class JobData implements IAgentDialogData {
public jobSchedules: sqlops.AgentJobScheduleInfo[];
public alerts: sqlops.AgentAlertInfo[];
public jobId: string;
public startStepId: number;
constructor(
ownerUri: string,
@@ -60,10 +61,11 @@ export class JobData implements IAgentDialogData {
this.category = jobInfo.category;
this.description = jobInfo.description;
this.enabled = jobInfo.enabled;
this.jobSteps = jobInfo.JobSteps;
this.jobSchedules = jobInfo.JobSchedules;
this.alerts = jobInfo.Alerts;
this.jobSteps = jobInfo.jobSteps;
this.jobSchedules = jobInfo.jobSchedules;
this.alerts = jobInfo.alerts;
this.jobId = jobInfo.jobId;
this.startStepId = jobInfo.startStepId;
}
}
@@ -141,17 +143,17 @@ export class JobData implements IAgentDialogData {
name: this.name,
owner: this.owner,
description: this.description,
EmailLevel: this.emailLevel,
PageLevel: this.pageLevel,
EventLogLevel: this.eventLogLevel,
DeleteLevel: this.deleteLevel,
OperatorToEmail: this.operatorToEmail,
OperatorToPage: this.operatorToPage,
emailLevel: this.emailLevel,
pageLevel: this.pageLevel,
eventLogLevel: this.eventLogLevel,
deleteLevel: this.deleteLevel,
operatorToEmail: this.operatorToEmail,
operatorToPage: this.operatorToPage,
enabled: this.enabled,
category: this.category,
Alerts: this.alerts,
JobSchedules: this.jobSchedules,
JobSteps: this.jobSteps,
alerts: this.alerts,
jobSchedules: this.jobSchedules,
jobSteps: this.jobSteps,
// The properties below are not collected from UI
// We could consider using a seperate class for create job request
//
@@ -166,7 +168,8 @@ export class JobData implements IAgentDialogData {
categoryType: 1, // LocalJob, hard-coding the value, corresponds to the target tab in SSMS
lastRun: '',
nextRun: '',
jobId: this.jobId
jobId: this.jobId,
startStepId: this.startStepId
};
}
}

View File

@@ -20,6 +20,9 @@ export abstract class AgentDialog<T extends IAgentDialogData> {
public readonly onSuccess: vscode.Event<T> = this._onSuccess.event;
public dialog: sqlops.window.modelviewdialog.Dialog;
// Dialog Name for Telemetry
public dialogName: string;
constructor(public ownerUri: string, public model: T, public title: string) {
}
@@ -31,8 +34,9 @@ export abstract class AgentDialog<T extends IAgentDialogData> {
protected abstract async initializeDialog(dialog: sqlops.window.modelviewdialog.Dialog);
public async openDialog() {
this.dialog = sqlops.window.modelviewdialog.createDialog(this.title);
public async openDialog(dialogName?: string) {
let event = dialogName ? dialogName : null;
this.dialog = sqlops.window.modelviewdialog.createDialog(this.title, event);
await this.model.initialize();

View File

@@ -116,6 +116,10 @@ export class AlertDialog extends AgentDialog<AlertData> {
private static readonly DelayMinutesTextBoxLabel: string = localize('alertDialog.DelayMinutes', 'Delay Minutes');
private static readonly DelaySecondsTextBoxLabel: string = localize('alertDialog.DelaySeconds', 'Delay Seconds');
// Event Name strings
private readonly NewAlertDialog = 'NewAlertDialogOpen';
private readonly EditAlertDialog = 'EditAlertDialogOpened';
// UI Components
private generalTab: sqlops.window.modelviewdialog.DialogTab;
private responseTab: sqlops.window.modelviewdialog.DialogTab;
@@ -149,6 +153,7 @@ export class AlertDialog extends AgentDialog<AlertData> {
private delayMinutesTextBox: sqlops.InputBoxComponent;
private delaySecondsTextBox: sqlops.InputBoxComponent;
private isEdit: boolean = false;
private databases: string[];
private jobModel: JobData;
public jobId: string;
@@ -166,6 +171,8 @@ export class AlertDialog extends AgentDialog<AlertData> {
this.jobModel = jobModel;
this.jobId = this.jobId ? this.jobId : this.jobModel.jobId;
this.jobName = this.jobName ? this.jobName : this.jobModel.name;
this.isEdit = alertInfo ? true : false;
this.dialogName = this.isEdit ? this.EditAlertDialog : this.NewAlertDialog;
}
protected async initializeDialog(dialog: sqlops.window.modelviewdialog.Dialog) {

View File

@@ -42,11 +42,12 @@ export class JobDialog extends AgentDialog<JobData> {
private readonly StepsTable_TypeColumnString: string = localize('jobDialog.type', 'Type');
private readonly StepsTable_SuccessColumnString: string = localize('jobDialog.onSuccess', 'On Success');
private readonly StepsTable_FailureColumnString: string = localize('jobDialog.onFailure', 'On Failure');
private readonly NewStepButtonString: string = localize('jobDialog.new', 'New...');
private readonly EditStepButtonString: string = localize('jobDialog.edit', 'Edit');
private readonly DeleteStepButtonString: string = localize('jobDialog.delete', 'Delete');
private readonly NewStepButtonString: string = localize('jobDialog.new', 'New Step');
private readonly EditStepButtonString: string = localize('jobDialog.edit', 'Edit Step');
private readonly DeleteStepButtonString: string = localize('jobDialog.delete', 'Delete Step');
private readonly MoveStepUpButtonString: string = localize('jobDialog.moveUp', 'Move Step Up');
private readonly MoveStepDownButtonString: string = localize('jobDialog.moveDown', 'Move Step Down');
private readonly StartStepDropdownString: string = localize('jobDialog.startStepAt', 'Start step');
// Notifications tab strings
private readonly NotificationsTabTopLabelString: string = localize('jobDialog.notificationsTabTop', 'Actions to perform when the job completes');
@@ -67,6 +68,10 @@ export class JobDialog extends AgentDialog<JobData> {
private readonly AlertEnabledLabelString: string = localize('jobDialog.alertEnabledLabel', 'Enabled');
private readonly AlertTypeLabelString: string = localize('jobDialog.alertTypeLabel', 'Type');
// Event Name strings
private readonly NewJobDialogEvent: string = 'NewJobDialogOpened';
private readonly EditJobDialogEvent: string = 'EditJobDialogOpened';
// UI Components
private generalTab: sqlops.window.modelviewdialog.DialogTab;
private stepsTab: sqlops.window.modelviewdialog.DialogTab;
@@ -101,6 +106,7 @@ export class JobDialog extends AgentDialog<JobData> {
private eventLogConditionDropdown: sqlops.DropDownComponent;
private deleteJobCheckBox: sqlops.CheckBoxComponent;
private deleteJobConditionDropdown: sqlops.DropDownComponent;
private startStepDropdown: sqlops.DropDownComponent;
// Schedule tab controls
private schedulesTable: sqlops.TableComponent;
@@ -115,6 +121,7 @@ export class JobDialog extends AgentDialog<JobData> {
private steps: sqlops.AgentJobStepInfo[];
private schedules: sqlops.AgentJobScheduleInfo[];
private alerts: sqlops.AgentAlertInfo[] = [];
private startStepDropdownValues: sqlops.CategoryValue[] = [];
constructor(ownerUri: string, jobInfo: sqlops.AgentJobInfo = undefined) {
super(
@@ -125,6 +132,7 @@ export class JobDialog extends AgentDialog<JobData> {
this.schedules = this.model.jobSchedules ? this.model.jobSchedules : [];
this.alerts = this.model.alerts ? this.model.alerts : [];
this.isEdit = jobInfo ? true : false;
this.dialogName = this.isEdit ? this.EditJobDialogEvent : this.NewJobDialogEvent;
}
protected async initializeDialog() {
@@ -218,13 +226,20 @@ export class JobDialog extends AgentDialog<JobData> {
this.StepsTable_FailureColumnString
],
data: data,
height: 750
height: 650
}).component();
this.startStepDropdown = view.modelBuilder.dropDown().withProperties({ width: 180 }).component();
this.startStepDropdown.enabled = this.steps.length > 1 ? true : false;
this.steps.forEach((step) => {
this.startStepDropdownValues.push({ displayName: step.id + ': ' + step.stepName, name: step.id.toString() });
});
this.startStepDropdown.values = this.startStepDropdownValues;
this.moveStepUpButton = view.modelBuilder.button()
.withProperties({
label: this.MoveStepUpButtonString,
width: 80
width: 120
}).component();
this.moveStepDownButton = view.modelBuilder.button()
@@ -238,7 +253,7 @@ export class JobDialog extends AgentDialog<JobData> {
this.newStepButton = view.modelBuilder.button().withProperties({
label: this.NewStepButtonString,
width: 80
width: 140
}).component();
let stepDialog = new JobStepDialog(this.model.ownerUri, '' , this.model, null, true);
@@ -246,6 +261,11 @@ export class JobDialog extends AgentDialog<JobData> {
let stepInfo = JobStepData.convertToAgentJobStepInfo(step);
this.steps.push(stepInfo);
this.stepsTable.data = this.convertStepsToData(this.steps);
this.startStepDropdownValues = [];
this.steps.forEach((step) => {
this.startStepDropdownValues.push({ displayName: step.id + ': ' + step.stepName, name: step.id.toString() });
});
this.startStepDropdown.values = this.startStepDropdownValues;
});
this.newStepButton.onDidClick((e)=>{
if (this.nameTextBox.value && this.nameTextBox.value.length > 0) {
@@ -258,12 +278,12 @@ export class JobDialog extends AgentDialog<JobData> {
this.editStepButton = view.modelBuilder.button().withProperties({
label: this.EditStepButtonString,
width: 80
width: 140
}).component();
this.deleteStepButton = view.modelBuilder.button().withProperties({
label: this.DeleteStepButtonString,
width: 80
width: 140
}).component();
this.stepsTable.enabled = false;
@@ -271,41 +291,31 @@ export class JobDialog extends AgentDialog<JobData> {
this.deleteStepButton.enabled = false;
this.moveStepUpButton.onDidClick(() => {
if (this.stepsTable.selectedRows.length === 1) {
let rowNumber = this.stepsTable.selectedRows[0];
// if it's not the first step
if (rowNumber !== 0) {
let previousRow = rowNumber - 1;
let previousStep = this.steps[previousRow];
let previousStepId = this.steps[previousRow].id;
let currentStep = this.steps[rowNumber];
let currentStepId = this.steps[rowNumber].id;
this.steps[previousRow] = currentStep;
this.steps[rowNumber] = previousStep;
this.stepsTable.data = this.convertStepsToData(this.steps);
this.steps[previousRow].id = previousStepId;
this.steps[rowNumber].id = currentStepId;
}
}
let rowNumber = this.stepsTable.selectedRows[0];
let previousRow = rowNumber - 1;
let previousStep = this.steps[previousRow];
let previousStepId = this.steps[previousRow].id;
let currentStep = this.steps[rowNumber];
let currentStepId = this.steps[rowNumber].id;
this.steps[previousRow] = currentStep;
this.steps[rowNumber] = previousStep;
this.stepsTable.data = this.convertStepsToData(this.steps);
this.steps[previousRow].id = previousStepId;
this.steps[rowNumber].id = currentStepId;
});
this.moveStepDownButton.onDidClick(() => {
if (this.stepsTable.selectedRows.length === 1) {
let rowNumber = this.stepsTable.selectedRows[0];
// if it's not the last step
if (this.steps.length !== rowNumber + 1) {
let nextRow = rowNumber + 1;
let nextStep = this.steps[nextRow];
let nextStepId = this.steps[nextRow].id;
let currentStep = this.steps[rowNumber];
let currentStepId = this.steps[rowNumber].id;
this.steps[nextRow] = currentStep;
this.steps[rowNumber] = nextStep;
this.stepsTable.data = this.convertStepsToData(this.steps);
this.steps[nextRow].id = nextStepId;
this.steps[rowNumber].id = currentStepId;
}
}
let rowNumber = this.stepsTable.selectedRows[0];
let nextRow = rowNumber + 1;
let nextStep = this.steps[nextRow];
let nextStepId = this.steps[nextRow].id;
let currentStep = this.steps[rowNumber];
let currentStepId = this.steps[rowNumber].id;
this.steps[nextRow] = currentStep;
this.steps[rowNumber] = nextStep;
this.stepsTable.data = this.convertStepsToData(this.steps);
this.steps[nextRow].id = nextStepId;
this.steps[rowNumber].id = currentStepId;
});
this.editStepButton.onDidClick(() => {
@@ -321,6 +331,12 @@ export class JobDialog extends AgentDialog<JobData> {
}
}
this.stepsTable.data = this.convertStepsToData(this.steps);
this.startStepDropdownValues = [];
this.steps.forEach((step) => {
this.startStepDropdownValues.push({ displayName: step.id + ': ' + step.stepName, name: step.id.toString() });
});
this.startStepDropdown.values = this.startStepDropdownValues;
});
editStepDialog.openDialog();
}
@@ -337,30 +353,52 @@ export class JobDialog extends AgentDialog<JobData> {
delete steps[rowNumber];
let data = this.convertStepsToData(steps);
this.stepsTable.data = data;
this.startStepDropdownValues = [];
this.steps.forEach((step) => {
this.startStepDropdownValues.push({ displayName: step.id + ': ' + step.stepName, name: step.id.toString() });
});
this.startStepDropdown.values = this.startStepDropdownValues;
}
});
});
}
});
this.stepsTable.onRowSelected(() => {
this.stepsTable.onRowSelected((row) => {
// only let edit or delete steps if there's
// one step selection
if (this.stepsTable.selectedRows.length === 1) {
this.moveStepUpButton.enabled = true;
this.moveStepDownButton.enabled = true;
let rowNumber = this.stepsTable.selectedRows[0];
// if it's not the last step
if (this.steps.length !== rowNumber + 1) {
this.moveStepDownButton.enabled = true;
}
// if it's not the first step
if (rowNumber !== 0) {
this.moveStepUpButton.enabled = true;
}
this.deleteStepButton.enabled = true;
this.editStepButton.enabled = true;
}
});
let formModel = view.modelBuilder.formContainer()
.withFormItems([{
let stepMoveContainer = this.createRowContainer(view).withItems([this.startStepDropdown, this.moveStepUpButton, this.moveStepDownButton]).component();
let stepsDialogContainer = this.createRowContainer(view).withItems([this.newStepButton, this.editStepButton, this.deleteStepButton]).component();
let formModel = view.modelBuilder.formContainer().withFormItems([
{
component: this.stepsTable,
title: this.JobStepsTopLabelString,
actions: [this.moveStepUpButton, this.moveStepDownButton, this.newStepButton, this.editStepButton, this.deleteStepButton]
}]).withLayout({ width: '100%' }).component();
title: this.JobStepsTopLabelString
},
{
component: stepMoveContainer,
title: this.StartStepDropdownString
},
{
component: stepsDialogContainer,
title: ''
}
]).withLayout({ width: '100%' }).component();
await view.initializeModel(formModel);
this.setConditionDropdownSelectedValue(this.startStepDropdown, this.model.startStepId);
});
}
@@ -623,6 +661,7 @@ export class JobDialog extends AgentDialog<JobData> {
this.model.pageLevel = this.getActualConditionValue(this.pagerCheckBox, this.pagerConditionDropdown);
this.model.eventLogLevel = this.getActualConditionValue(this.eventLogCheckBox, this.eventLogConditionDropdown);
this.model.deleteLevel = this.getActualConditionValue(this.deleteJobCheckBox, this.deleteJobConditionDropdown);
this.model.startStepId = +this.getDropdownValue(this.startStepDropdown);
if (!this.model.jobSteps) {
this.model.jobSteps = [];
}

View File

@@ -67,6 +67,9 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
private readonly QuitJobReportingSuccess: string = localize('jobStepDialog.quitJobSuccess', 'Quit the job reporting success');
private readonly QuitJobReportingFailure: string = localize('jobStepDialog.quitJobFailure', 'Quit the job reporting failure');
// Event Name strings
private readonly NewStepDialog = 'NewStepDialogOpened';
private readonly EditStepDialog = 'EditStepDialogOpened';
// UI Components
// Dialogs
@@ -131,6 +134,7 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
this.jobModel = jobModel;
this.jobName = this.jobName ? this.jobName : this.jobModel.name;
this.server = server;
this.dialogName = this.isEdit ? this.EditStepDialog : this.NewStepDialog;
}
private initializeUIComponents() {

View File

@@ -43,6 +43,10 @@ export class OperatorDialog extends AgentDialog<OperatorData> {
private static readonly AlertEmailColumnLabel: string = localize('createOperator.AlertEmailColumnLabel', 'E-mail');
private static readonly AlertPagerColumnLabel: string = localize('createOperator.AlertPagerColumnLabel', 'Pager');
// Event strings
private readonly NewOperatorDialog = 'NewOperatorDialogOpened';
private readonly EditOperatorDialog = 'EditOperatorDialogOpened';
// UI Components
private generalTab: sqlops.window.modelviewdialog.DialogTab;
private notificationsTab: sqlops.window.modelviewdialog.DialogTab;
@@ -68,12 +72,15 @@ export class OperatorDialog extends AgentDialog<OperatorData> {
// Notification tab controls
private alertsTable: sqlops.TableComponent;
private isEdit: boolean = false;
constructor(ownerUri: string, operatorInfo: sqlops.AgentOperatorInfo = undefined) {
super(
ownerUri,
new OperatorData(ownerUri, operatorInfo),
operatorInfo ? OperatorDialog.EditDialogTitle : OperatorDialog.CreateDialogTitle);
this.isEdit = operatorInfo ? true : false;
this.dialogName = this.isEdit ? this.EditOperatorDialog : this.NewOperatorDialog;
}
protected async initializeDialog(dialog: sqlops.window.modelviewdialog.Dialog) {

View File

@@ -36,6 +36,9 @@ export class ProxyDialog extends AgentDialog<ProxyData> {
private static readonly PowerShellLabel: string = localize('createProxy.PowerShell', 'PowerShell');
private static readonly SubSystemHeadingLabel: string = localize('createProxy.subSystemHeading', 'Active to the following subsytems');
private readonly NewProxyDialog = 'NewProxyDialogOpened';
private readonly EditProxyDialog = 'EditProxyDialogOpened';
// UI Components
private generalTab: sqlops.window.modelviewdialog.DialogTab;
@@ -56,6 +59,7 @@ export class ProxyDialog extends AgentDialog<ProxyData> {
private powershellCheckBox: sqlops.CheckBoxComponent;
private credentials: sqlops.CredentialInfo[];
private isEdit: boolean = false;
constructor(ownerUri: string, proxyInfo: sqlops.AgentProxyInfo = undefined, credentials: sqlops.CredentialInfo[]) {
super(
@@ -63,6 +67,8 @@ export class ProxyDialog extends AgentDialog<ProxyData> {
new ProxyData(ownerUri, proxyInfo),
proxyInfo ? ProxyDialog.EditDialogTitle : ProxyDialog.CreateDialogTitle);
this.credentials = credentials;
this.isEdit = proxyInfo ? true : false;
this.dialogName = this.isEdit ? this.EditProxyDialog : this.NewProxyDialog;
}
protected async initializeDialog(dialog: sqlops.window.modelviewdialog.Dialog) {

View File

@@ -40,13 +40,13 @@ export class MainController {
public activate(): void {
vscode.commands.registerCommand('agent.openJobDialog', (ownerUri: string, jobInfo: sqlops.AgentJobInfo) => {
let dialog = new JobDialog(ownerUri, jobInfo);
dialog.openDialog();
dialog.dialogName ? dialog.openDialog(dialog.dialogName) : dialog.openDialog();
});
vscode.commands.registerCommand('agent.openNewStepDialog', (ownerUri: string, server: string, jobInfo: sqlops.AgentJobInfo, jobStepInfo: sqlops.AgentJobStepInfo) => {
AgentUtils.getAgentService().then((agentService) => {
let jobData: JobData = new JobData(ownerUri, jobInfo, agentService);
let dialog = new JobStepDialog(ownerUri, server, jobData, jobStepInfo, false);
dialog.openDialog();
dialog.dialogName ? dialog.openDialog(dialog.dialogName) : dialog.openDialog();
});
});
vscode.commands.registerCommand('agent.openPickScheduleDialog', (ownerUri: string, jobName: string) => {
@@ -57,17 +57,16 @@ export class MainController {
AgentUtils.getAgentService().then((agentService) => {
let jobData: JobData = new JobData(ownerUri, jobInfo, agentService);
let dialog = new AlertDialog(ownerUri, jobData, alertInfo, false);
dialog.openDialog();
dialog.dialogName ? dialog.openDialog(dialog.dialogName) : dialog.openDialog();
});
});
vscode.commands.registerCommand('agent.openOperatorDialog', (ownerUri: string, operatorInfo: sqlops.AgentOperatorInfo) => {
let dialog = new OperatorDialog(ownerUri, operatorInfo);
dialog.openDialog();
dialog.dialogName ? dialog.openDialog(dialog.dialogName) : dialog.openDialog();
});
vscode.commands.registerCommand('agent.openProxyDialog', (ownerUri: string, proxyInfo: sqlops.AgentProxyInfo, credentials: sqlops.CredentialInfo[]) => {
let dialog = new ProxyDialog(ownerUri, proxyInfo, credentials);
dialog.openDialog();
MainController.showNotYetImplemented();
dialog.dialogName ? dialog.openDialog(dialog.dialogName) : dialog.openDialog();
});
}

View File

@@ -2,7 +2,7 @@
"name": "import",
"displayName": "SQL Server Import",
"description": "SQL Server Import for Azure Data Studio supports importing CSV or JSON files into SQL Server.",
"version": "0.4.2",
"version": "0.5.0",
"publisher": "Microsoft",
"preview": true,
"engines": {

View File

@@ -1,6 +1,6 @@
{
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
"version": "1.5.0-alpha.60",
"version": "1.5.0-alpha.63",
"downloadFileNames": {
"Windows_86": "win-x86-netcoreapp2.2.zip",
"Windows_64": "win-x64-netcoreapp2.2.zip",

View File

@@ -0,0 +1,17 @@
# Notebook extension for Azure Data Studio
Welcome to the Notebook extension for Azure Data Studio! This extension supports core notebook functionality including configuration settings, actions such as New / Open Notebook, and more.
## Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
## Privacy Statement
The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) describes the privacy statement of this software.
## License
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the [Source EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt).

View File

@@ -0,0 +1,71 @@
{
"name": "notebook",
"displayName": "%displayName%",
"description": "%description%",
"version": "0.1.0",
"publisher": "Microsoft",
"engines": {
"vscode": "*",
"sqlops": "*"
},
"main": "./out/extension",
"activationEvents": [
"*"
],
"contributes": {
"configuration": {
"type": "object",
"title": "%notebook.configuration.title%",
"properties": {
"notebook.enabled": {
"type": "boolean",
"default": true,
"description": "%notebook.enabled.description%"
}
}
},
"commands": [
{
"command": "notebook.command.new",
"title": "%notebook.command.new%",
"icon": {
"dark": "resources/dark/new_notebook_inverse.svg",
"light": "resources/light/new_notebook.svg"
}
},
{
"command": "notebook.command.open",
"title": "%notebook.command.open%",
"icon": {
"dark": "resources/dark/open_notebook_inverse.svg",
"light": "resources/light/open_notebook.svg"
}
}
],
"menus": {
"commandPalette": [
{
"command": "notebook.command.new",
"when": "config.notebook.enabled"
},
{
"command": "notebook.command.open",
"when": "config.notebook.enabled"
}
]
},
"keybindings": [
{
"command": "notebook.command.new",
"key": "Ctrl+Shift+N",
"when": "config.notebook.enabled"
}
]
},
"dependencies": {
"vscode-nls": "^4.0.0"
},
"devDependencies": {
"@types/node": "8.0.33"
}
}

View File

@@ -0,0 +1,8 @@
{
"displayName": "Notebook Core Extensions",
"description": "Defines the Data-procotol based Notebook contribution and many Notebook commands and contributions.",
"notebook.configuration.title": "Notebook configuration",
"notebook.enabled.description": "Enable viewing notebook files using built-in notebook editor.",
"notebook.command.new": "New Notebook",
"notebook.command.open": "Open Notebook"
}

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;}.cls-2{fill:#388a34;}</style></defs><title>new_notebook_inverse</title><path class="cls-1" d="M11.87,1.24V.33H9.13A3.78,3.78,0,0,0,7.92.52a3.48,3.48,0,0,0-1.07.58A3.6,3.6,0,0,0,5.78.52,3.78,3.78,0,0,0,4.57.33H1.83v.91H0V13.1H9.67v-.91H7a4,4,0,0,1,.47-.39A2.39,2.39,0,0,1,8,11.52a2.2,2.2,0,0,1,.53-.18,2.93,2.93,0,0,1,.61-.06h2.74V2.15h.91V9h.91V1.24Zm-9.13,0H4.57a3,3,0,0,1,1,.17,2.58,2.58,0,0,1,.85.49v8.93a3.94,3.94,0,0,0-.88-.35,3.73,3.73,0,0,0-.94-.12H2.74Zm-1.82,11v-10h.91v9.13H4.57a2.93,2.93,0,0,1,.61.06,2.55,2.55,0,0,1,.53.18,2.68,2.68,0,0,1,.49.28,3.29,3.29,0,0,1,.46.39Zm8.21-1.83a3.73,3.73,0,0,0-.94.12,4.22,4.22,0,0,0-.89.35V1.9a2.74,2.74,0,0,1,.86-.49,2.91,2.91,0,0,1,1-.17H11v9.12ZM12.87,10v2.2h-2.2v.91h3V10Z"/><polygon class="cls-2" points="16 12.19 16 13.13 13.8 13.13 13.8 15.33 12.87 15.33 12.87 13.13 10.67 13.13 10.67 12.19 12.87 12.19 12.87 9.99 13.8 9.99 13.8 12.19 16 12.19"/><path class="cls-2" d="M13.8,12.19V10h-.93v2.2h-2.2v.94h2.2v2.2h.93v-2.2H16v-.94Z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

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;}.cls-2{fill:#0095d7;}</style></defs><title>open_notebook_inverse</title><path class="cls-1" d="M12.55,4.21l-.08-.11h-.56l-.69.06a1.54,1.54,0,0,0-.23.29v8.69H9.18a3.32,3.32,0,0,0-.93.13,3.34,3.34,0,0,0-.87.34V4.76a2.88,2.88,0,0,1,.43-.31A5.58,5.58,0,0,1,8.29,3.3a2.63,2.63,0,0,0-.3.09A3.62,3.62,0,0,0,6.93,4a3.68,3.68,0,0,0-1.07-.57A3.58,3.58,0,0,0,4.67,3.2H2v.9H.15V15.85H13.72V5.48ZM2.86,4.1H4.67a2.61,2.61,0,0,1,1,.17,2.32,2.32,0,0,1,.86.49v8.85a3.27,3.27,0,0,0-.88-.34,3.22,3.22,0,0,0-.93-.13H2.86ZM1,15V5H2v9H4.67a3.94,3.94,0,0,1,.61.06,3.2,3.2,0,0,1,.52.18,4.19,4.19,0,0,1,.49.29,2.28,2.28,0,0,1,.45.39ZM12.8,15H7.11a2.7,2.7,0,0,1,.47-.39A2.83,2.83,0,0,1,8,14.28a3.42,3.42,0,0,1,.54-.18A3.81,3.81,0,0,1,9.18,14h2.73V5h.89Z"/><polygon class="cls-2" points="13.2 3.56 13.2 3.58 13.19 3.57 13.2 3.56"/><path class="cls-2" d="M13.19,3.57h0v0Z"/><polygon class="cls-2" points="13.2 3.56 13.2 3.58 13.19 3.57 13.2 3.56"/><polygon class="cls-2" points="14.21 1.65 14.19 1.65 14.19 1.63 14.21 1.65"/><path class="cls-2" d="M15.91,2.1,14.2,3.81l-.38.38-.62-.61v0l1-1H12.79a3.35,3.35,0,0,0-1.09.26h0a3.94,3.94,0,0,0-.86.52l-.24.21s0,0,0,0a3.3,3.3,0,0,0-.51.67,3.1,3.1,0,0,0-.26.47A3.41,3.41,0,0,0,9.5,6.11H8.6a4.68,4.68,0,0,1,.16-1.19A4.74,4.74,0,0,1,9,4.26a2.21,2.21,0,0,1,.2-.41,4.66,4.66,0,0,1,.36-.51c.1-.13.22-.26.34-.39a4.14,4.14,0,0,1,.66-.53,1.19,1.19,0,0,1,.23-.16,2.79,2.79,0,0,1,.34-.18l.31-.13.42-.14a4.32,4.32,0,0,1,1.19-.16h1.15l-1-1L13.82,0Z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

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:#388a34;}</style></defs><title>new_notebook</title><path d="M11.86,1.24V.33H9.13A3.78,3.78,0,0,0,7.91.52a3.48,3.48,0,0,0-1.07.58A3.6,3.6,0,0,0,5.78.52,3.78,3.78,0,0,0,4.57.33H1.83v.91H0V13.1H9.66v-.91H7a4,4,0,0,1,.47-.39A2.39,2.39,0,0,1,8,11.52a2.2,2.2,0,0,1,.53-.18,2.93,2.93,0,0,1,.61-.06h2.74V2.15h.91V9h.91V1.24Zm-9.13,0H4.57a3,3,0,0,1,1,.17,2.58,2.58,0,0,1,.85.49v8.93a3.94,3.94,0,0,0-.88-.35,3.73,3.73,0,0,0-.94-.12H2.73Zm-1.82,11v-10h.91v9.13H4.57a2.93,2.93,0,0,1,.61.06,2.55,2.55,0,0,1,.53.18,2.68,2.68,0,0,1,.49.28,3.29,3.29,0,0,1,.46.39Zm8.21-1.83a3.73,3.73,0,0,0-.94.12,4.22,4.22,0,0,0-.89.35V1.9a2.74,2.74,0,0,1,.86-.49,2.91,2.91,0,0,1,1-.17h1.82v9.12ZM12.86,10v2.2h-2.2v.91h3V10Z"/><polygon class="cls-1" points="15.99 12.19 15.99 13.13 13.79 13.13 13.79 15.33 12.87 15.33 12.87 13.13 10.66 13.13 10.66 12.19 12.87 12.19 12.87 9.99 13.79 9.99 13.79 12.19 15.99 12.19"/><path class="cls-1" d="M13.79,12.19V10h-.93v2.2h-2.2v.94h2.2v2.2h.93v-2.2H16v-.94Z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

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:#00539c;}</style></defs><title>open_notebook</title><path d="M12.4,4.21l-.08-.11h-.56l-.69.06a1.54,1.54,0,0,0-.23.29v8.69H9a3.32,3.32,0,0,0-.93.13,3.34,3.34,0,0,0-.87.34V4.76a2.88,2.88,0,0,1,.43-.31A5.58,5.58,0,0,1,8.14,3.3a2.63,2.63,0,0,0-.3.09A3.62,3.62,0,0,0,6.78,4a3.68,3.68,0,0,0-1.07-.57A3.58,3.58,0,0,0,4.52,3.2H1.81v.9H0V15.85H13.57V5.48ZM2.71,4.1H4.52a2.61,2.61,0,0,1,1,.17,2.32,2.32,0,0,1,.86.49v8.85a3.27,3.27,0,0,0-.88-.34,3.22,3.22,0,0,0-.93-.13H2.71ZM.9,15V5h.91v9H4.52a3.94,3.94,0,0,1,.61.06,3.2,3.2,0,0,1,.52.18,4.19,4.19,0,0,1,.49.29,2.28,2.28,0,0,1,.45.39Zm11.75,0H7a2.7,2.7,0,0,1,.47-.39,2.83,2.83,0,0,1,.47-.29,3.42,3.42,0,0,1,.54-.18A3.81,3.81,0,0,1,9,14h2.73V5h.89Z"/><polygon class="cls-1" points="13.05 3.56 13.05 3.58 13.04 3.57 13.05 3.56"/><path class="cls-1" d="M13,3.57h0v0Z"/><polygon class="cls-1" points="13.05 3.56 13.05 3.58 13.04 3.57 13.05 3.56"/><polygon class="cls-1" points="14.06 1.65 14.04 1.65 14.04 1.63 14.06 1.65"/><path class="cls-1" d="M15.76,2.1,14,3.81l-.38.38L13,3.58v0l1-1H12.64a3.35,3.35,0,0,0-1.09.26h0a3.94,3.94,0,0,0-.86.52l-.24.21s0,0,0,0a3.3,3.3,0,0,0-.51.67,3.1,3.1,0,0,0-.26.47,3.41,3.41,0,0,0-.27,1.39h-.9a4.68,4.68,0,0,1,.16-1.19,4.74,4.74,0,0,1,.25-.66,2.21,2.21,0,0,1,.2-.41,4.66,4.66,0,0,1,.36-.51c.1-.13.22-.26.34-.39a4.14,4.14,0,0,1,.66-.53,1.19,1.19,0,0,1,.23-.16A2.79,2.79,0,0,1,11,2.08l.31-.13.42-.14a4.32,4.32,0,0,1,1.19-.16h1.15l-1-1L13.67,0Z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

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.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
let counter = 0;
export function activate(extensionContext: vscode.ExtensionContext) {
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.new', () => {
let title = `Untitled-${counter++}`;
let untitledUri = vscode.Uri.parse(`untitled:${title}`);
sqlops.nb.showNotebookDocument(untitledUri).then(success => {
}, (err: Error) => {
vscode.window.showErrorMessage(err.message);
});
}));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.open', () => {
openNotebook();
}));
}
async function openNotebook(): Promise<void> {
try {
let filter = {};
// TODO support querying valid notebook file types
filter[localize('notebookFiles', 'Notebooks')] = ['ipynb'];
let file = await vscode.window.showOpenDialog({
filters: filter
});
if (file) {
let doc = await vscode.workspace.openTextDocument(file[0]);
vscode.window.showTextDocument(doc);
}
} catch (err) {
vscode.window.showErrorMessage(err);
}
}
// this method is called when your extension is deactivated
export function deactivate() {
}

View File

@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../src/sql/sqlops.d.ts'/>
/// <reference path='../../../../src/sql/sqlops.proposed.d.ts'/>
/// <reference path='../../../../src/vs/vscode.d.ts'/>
/// <reference types='@types/node'/>

View File

@@ -0,0 +1,22 @@
{
"compileOnSave": true,
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "./out",
"lib": [
"es6", "es2015.promise"
],
"typeRoots": [
"./node_modules/@types"
],
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"declaration": true
},
"exclude": [
"node_modules"
]
}

View File

@@ -0,0 +1,13 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/node@8.0.33":
version "8.0.33"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.33.tgz#1126e94374014e54478092830704f6ea89df04cd"
integrity sha512-vmCdO8Bm1ExT+FWfC9sd9r4jwqM7o97gGy2WBshkkXbf/2nLAJQUrZfIhw27yVOtLUev6kSZc4cav/46KbDd8A==
vscode-nls@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002"
integrity sha512-qCfdzcH+0LgQnBpZA53bA32kzp9rpq/f66Som577ObeuDlFIrtbEJ+A/+CCxjIh4G8dpJYNCKIsxpRAHIfsbNw==

View File

@@ -2,7 +2,7 @@
"name": "profiler",
"displayName": "SQL Server Profiler",
"description": "SQL Server Profiler for Azure Data Studio",
"version": "0.5.1",
"version": "0.6.0",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",

View File

@@ -1,6 +1,6 @@
{
"name": "azuredatastudio",
"version": "1.3.6",
"version": "1.3.7",
"distro": "8c3e97e3425cc9814496472ab73e076de2ba99ee",
"author": {
"name": "Microsoft Corporation"
@@ -90,6 +90,7 @@
"@types/mocha": "2.2.39",
"@types/sanitize-html": "^1.18.2",
"@types/semver": "5.3.30",
"@types/should": "^13.0.0",
"@types/sinon": "1.16.34",
"@types/winreg": "^1.2.30",
"asar": "^0.14.0",
@@ -148,8 +149,10 @@
"queue": "3.0.6",
"remap-istanbul": "^0.6.4",
"rimraf": "^2.2.8",
"should": "^13.2.3",
"sinon": "^1.17.2",
"source-map": "^0.4.4",
"temp-write": "^3.4.0",
"tslint": "^5.9.1",
"typemoq": "^0.3.2",
"typescript": "2.9.2",

View File

@@ -25,7 +25,6 @@ export const NewQuery = 'NewQuery';
export const FirewallRuleRequested = 'FirewallRuleCreated';
export const DashboardNavigated = 'DashboardNavigated';
// Telemetry Properties
// Modal Dialogs:
@@ -42,3 +41,21 @@ export const Accounts = 'Accounts';
export const FireWallRule = 'FirewallRule';
export const AutoOAuth = 'AutoOAuth';
export const AddNewDashboardTab = 'AddNewDashboardTab';
// SQL Agent Events:
// Views
export const JobsView = 'JobsViewOpened';
export const JobHistoryView = 'JobHistoryViewOpened';
export const JobStepsView = 'JobStepsViewOpened';
// Actions
export const RunAgentJob = 'RunAgentJob';
export const StopAgentJob = 'StopAgentJob';
export const DeleteAgentJob = 'DeleteAgentJob';
export const DeleteAgentJobStep = 'DeleteAgentJobStep';
export const DeleteAgentAlert = 'DeleteAgentAlert';
export const DeleteAgentOperator = 'DeleteAgentOperator';
export const DeleteAgentProxy = 'DeleteAgentProxy';

View File

@@ -4,31 +4,10 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as crypto from 'crypto';
import * as os from 'os';
import { ITelemetryService, ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { warn } from 'sql/base/common/log';
import { generateUuid } from 'vs/base/common/uuid';
// Generate a unique, deterministic ID for the current user of the extension
export function generateUserId(): Promise<string> {
return new Promise<string>(resolve => {
try {
getmac.getMac((error, macAddress) => {
if (!error) {
resolve(crypto.createHash('sha256').update(macAddress + os.homedir(), 'utf8').digest('hex'));
} else {
resolve(generateUuid()); // fallback
}
});
} catch (err) {
resolve(generateUuid()); // fallback
}
});
}
export interface IConnectionTelemetryData extends ITelemetryData {
provider?: string;
}

View File

@@ -19,6 +19,7 @@ import { QueryPlanInput } from 'sql/parts/queryPlan/queryPlanInput';
import { NotebookInput, NotebookInputModel, NotebookInputValidator } from 'sql/parts/notebook/notebookInput';
import { Extensions, INotebookProviderRegistry } from 'sql/services/notebook/notebookRegistry';
import { DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService';
import { getProviderForFileName } from 'sql/parts/notebook/notebookUtils';
const fs = require('fs');
@@ -183,17 +184,6 @@ function getNotebookFileExtensions() {
return notebookRegistry.getSupportedFileExtensions();
}
function getProviderForFileName(fileName: string) {
let fileExt = path.extname(fileName);
if (fileExt && fileExt.startsWith('.')) {
fileExt = fileExt.slice(1,fileExt.length);
let notebookRegistry = Registry.as<INotebookProviderRegistry>(Extensions.NotebookProviderContribution);
return notebookRegistry.getProviderForFileType(fileExt);
}
return DEFAULT_NOTEBOOK_PROVIDER;
}
/**
* Checks whether the given EditorInput is set to either undefined or sql mode
* @param input The EditorInput to check the mode of

View File

@@ -769,8 +769,19 @@ export class ConnectionManagementService extends Disposable implements IConnecti
return false;
}
}
let tokens = await this._accountManagementService.getSecurityToken(account, AzureResource.Sql);
connection.options['azureAccountToken'] = Object.values(tokens)[0].token;
let tokensByTenant = await this._accountManagementService.getSecurityToken(account, AzureResource.Sql);
let token: string;
let tenantId = connection.azureTenantId;
if (tenantId && tokensByTenant[tenantId]) {
token = tokensByTenant[tenantId].token;
} else {
let tokens = Object.values(tokensByTenant);
if (tokens.length === 0) {
return false;
}
token = Object.values(tokensByTenant)[0].token;
}
connection.options['azureAccountToken'] = token;
connection.options['password'] = '';
return true;
}

View File

@@ -42,6 +42,7 @@ export class ConnectionProfile extends ProviderConnectionInfo implements interfa
this.savePassword = model.savePassword;
this.saveProfile = model.saveProfile;
this._id = model.id;
this.azureTenantId = model.azureTenantId;
} else {
//Default for a new connection
this.savePassword = false;
@@ -84,6 +85,14 @@ export class ConnectionProfile extends ProviderConnectionInfo implements interfa
this._id = value;
}
public get azureTenantId(): string {
return this.options['azureTenantId'];
}
public set azureTenantId(value: string) {
this.options['azureTenantId'] = value;
}
public get groupFullName(): string {
return this._groupName;
}
@@ -159,7 +168,8 @@ export class ConnectionProfile extends ProviderConnectionInfo implements interfa
userName: this.userName,
options: this.options,
saveProfile: this.saveProfile,
id: this.id
id: this.id,
azureTenantId: this.azureTenantId
};
return result;

View File

@@ -52,9 +52,11 @@ export class ConnectionWidget {
private _password: string;
private _rememberPasswordCheckBox: Checkbox;
private _azureAccountDropdown: SelectBox;
private _azureTenantDropdown: SelectBox;
private _refreshCredentialsLinkBuilder: Builder;
private _addAzureAccountMessage: string = localize('connectionWidget.AddAzureAccount', 'Add an account...');
private readonly _azureProviderId = 'azurePublicCloud';
private _azureTenantId: string;
private _azureAccountList: sqlops.Account[];
private _advancedButton: Button;
private _callbacks: IConnectionComponentCallbacks;
@@ -215,6 +217,12 @@ export class ConnectionWidget {
let refreshCredentialsBuilder = DialogHelper.appendRow(this._tableContainer, '', 'connection-label', 'connection-input', 'azure-account-row refresh-credentials-link');
this._refreshCredentialsLinkBuilder = refreshCredentialsBuilder.a({ href: '#' }).text(localize('connectionWidget.refreshAzureCredentials', 'Refresh account credentials'));
// Azure tenant picker
let tenantLabel = localize('connection.azureTenantDropdownLabel', 'Azure AD tenant');
let tenantDropdownBuilder = DialogHelper.appendRow(this._tableContainer, tenantLabel, 'connection-label', 'connection-input', 'azure-account-row azure-tenant-row');
this._azureTenantDropdown = new SelectBox([], undefined, this._contextViewService, tenantDropdownBuilder.getContainer(), { ariaLabel: tenantLabel });
DialogHelper.appendInputSelectBox(tenantDropdownBuilder, this._azureTenantDropdown);
// Database
let databaseOption = this._optionsMaps[ConnectionOptionSpecialType.databaseName];
let databaseNameBuilder = DialogHelper.appendRow(this._tableContainer, databaseOption.displayName, 'connection-label', 'connection-input');
@@ -308,6 +316,13 @@ export class ConnectionWidget {
}));
}
if (this._azureTenantDropdown) {
this._toDispose.push(styler.attachSelectBoxStyler(this._azureTenantDropdown, this._themeService));
this._toDispose.push(this._azureTenantDropdown.onDidSelect((selectInfo) => {
this.onAzureTenantSelected(selectInfo.index);
}));
}
if (this._refreshCredentialsLinkBuilder) {
this._toDispose.push(this._refreshCredentialsLinkBuilder.on(DOM.EventType.CLICK, async () => {
let account = this._azureAccountList.find(account => account.key.accountId === this._azureAccountDropdown.value);
@@ -426,7 +441,7 @@ export class ConnectionWidget {
accountDropdownOptions.push(this._addAzureAccountMessage);
this._azureAccountDropdown.setOptions(accountDropdownOptions);
this._azureAccountDropdown.selectWithOptionName(oldSelection);
this.updateRefreshCredentialsLink();
await this.onAzureAccountSelected();
}
private async updateRefreshCredentialsLink(): Promise<void> {
@@ -441,7 +456,6 @@ export class ConnectionWidget {
private async onAzureAccountSelected(): Promise<void> {
// Reset the dropdown's validation message if the old selection was not valid but the new one is
this.validateAzureAccountSelection(false);
this._refreshCredentialsLinkBuilder.display('none');
// Open the add account dialog if needed, then select the added account
if (this._azureAccountDropdown.value === this._addAzureAccountMessage) {
@@ -461,6 +475,35 @@ export class ConnectionWidget {
}
this.updateRefreshCredentialsLink();
// Display the tenant select box if needed
const hideTenantsClassName = 'hide-azure-tenants';
let selectedAccount = this._azureAccountList.find(account => account.key.accountId === this._azureAccountDropdown.value);
if (selectedAccount && selectedAccount.properties.tenants && selectedAccount.properties.tenants.length > 1) {
// There are multiple tenants available so let the user select one
let options = selectedAccount.properties.tenants.map(tenant => tenant.displayName);
this._azureTenantDropdown.setOptions(options);
this._tableContainer.getContainer().classList.remove(hideTenantsClassName);
this.onAzureTenantSelected(0);
} else {
if (selectedAccount && selectedAccount.properties.tenants && selectedAccount.properties.tenants.length === 1) {
this._azureTenantId = selectedAccount.properties.tenants[0].id;
} else {
this._azureTenantId = undefined;
}
this._tableContainer.getContainer().classList.add(hideTenantsClassName);
}
}
private onAzureTenantSelected(tenantIndex: number): void {
this._azureTenantId = undefined;
let account = this._azureAccountList.find(account => account.key.accountId === this._azureAccountDropdown.value);
if (account && account.properties.tenants) {
let tenant = account.properties.tenants[tenantIndex];
if (tenant) {
this._azureTenantId = tenant.id;
}
}
}
private serverNameChanged(serverName: string) {
@@ -518,6 +561,7 @@ export class ConnectionWidget {
this._passwordInputBox.value = connectionInfo.password ? Constants.passwordChars : '';
this._password = this.getModelValue(connectionInfo.password);
this._saveProfile = connectionInfo.saveProfile;
this._azureTenantId = connectionInfo.azureTenantId;
let groupName: string;
if (this._saveProfile) {
if (!connectionInfo.groupFullName) {
@@ -551,6 +595,22 @@ export class ConnectionWidget {
tableContainerElement.classList.add('hide-azure-accounts');
}
if (this.authType === AuthenticationType.AzureMFA) {
this.fillInAzureAccountOptions().then(async () => {
this._azureAccountDropdown.selectWithOptionName(this.getModelValue(connectionInfo.userName));
await this.onAzureAccountSelected();
let tenantId = connectionInfo.azureTenantId;
let account = this._azureAccountList.find(account => account.key.accountId === this._azureAccountDropdown.value);
if (account && account.properties.tenants.length > 1) {
let tenant = account.properties.tenants.find(tenant => tenant.id === tenantId);
if (tenant) {
this._azureTenantDropdown.selectWithOptionName(tenant.displayName);
}
this.onAzureTenantSelected(this._azureTenantDropdown.values.indexOf(this._azureTenantDropdown.value));
}
});
}
// Disable connect button if -
// 1. Authentication type is SQL Login and no username is provided
// 2. No server name is provided
@@ -716,6 +776,9 @@ export class ConnectionWidget {
model.saveProfile = true;
model.groupId = this.findGroupId(model.groupFullName);
}
if (this.authType === AuthenticationType.AzureMFA) {
model.azureTenantId = this._azureTenantId;
}
}
return validInputs;
}

View File

@@ -128,3 +128,7 @@
.hide-refresh-link .azure-account-row.refresh-credentials-link {
display: none;
}
.hide-azure-tenants .azure-tenant-row {
display: none;
}

View File

@@ -17,6 +17,9 @@ import { AlertsViewComponent } from 'sql/parts/jobManagement/views/alertsView.co
import { OperatorsViewComponent } from 'sql/parts/jobManagement/views/operatorsView.component';
import { ProxiesViewComponent } from 'sql/parts/jobManagement/views/proxiesView.component';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
import { telemetryURIDescriptor } from 'vs/platform/telemetry/common/telemetryUtils';
export enum JobActions {
Run = 'run',
@@ -80,7 +83,8 @@ export class RunJobAction extends Action {
constructor(
@INotificationService private notificationService: INotificationService,
@IJobManagementService private jobManagementService: IJobManagementService,
@IInstantiationService private instantationService: IInstantiationService
@IInstantiationService private instantationService: IInstantiationService,
@ITelemetryService private telemetryService: ITelemetryService
) {
super(RunJobAction.ID, RunJobAction.LABEL, 'runJobIcon');
}
@@ -89,6 +93,7 @@ export class RunJobAction extends Action {
let jobName = context.agentJobInfo.name;
let ownerUri = context.ownerUri;
let refreshAction = this.instantationService.createInstance(JobsRefreshAction);
this.telemetryService.publicLog(TelemetryKeys.RunAgentJob);
return new TPromise<boolean>((resolve, reject) => {
this.jobManagementService.jobAction(ownerUri, jobName, JobActions.Run).then(result => {
if (result.success) {
@@ -118,7 +123,8 @@ export class StopJobAction extends Action {
constructor(
@INotificationService private notificationService: INotificationService,
@IJobManagementService private jobManagementService: IJobManagementService,
@IInstantiationService private instantationService: IInstantiationService
@IInstantiationService private instantationService: IInstantiationService,
@ITelemetryService private telemetryService: ITelemetryService
) {
super(StopJobAction.ID, StopJobAction.LABEL, 'stopJobIcon');
}
@@ -127,6 +133,7 @@ export class StopJobAction extends Action {
let jobName = context.agentJobInfo.name;
let ownerUri = context.ownerUri;
let refreshAction = this.instantationService.createInstance(JobsRefreshAction);
this.telemetryService.publicLog(TelemetryKeys.StopAgentJob);
return new TPromise<boolean>((resolve, reject) => {
this.jobManagementService.jobAction(ownerUri, jobName, JobActions.Stop).then(result => {
if (result.success) {
@@ -174,7 +181,8 @@ export class DeleteJobAction extends Action {
constructor(
@INotificationService private _notificationService: INotificationService,
@IJobManagementService private _jobService: IJobManagementService
@IJobManagementService private _jobService: IJobManagementService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
super(DeleteJobAction.ID, DeleteJobAction.LABEL);
}
@@ -188,6 +196,7 @@ export class DeleteJobAction extends Action {
[{
label: DeleteJobAction.LABEL,
run: () => {
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentJob);
self._jobService.deleteJob(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteJob", "Could not delete job '{0}'.\nError: {1}",
@@ -234,7 +243,8 @@ export class DeleteStepAction extends Action {
constructor(
@INotificationService private _notificationService: INotificationService,
@IJobManagementService private _jobService: IJobManagementService,
@IInstantiationService private instantationService: IInstantiationService
@IInstantiationService private instantationService: IInstantiationService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
super(DeleteStepAction.ID, DeleteStepAction.LABEL);
}
@@ -249,6 +259,7 @@ export class DeleteStepAction extends Action {
[{
label: DeleteStepAction.LABEL,
run: () => {
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentJobStep);
self._jobService.deleteJobStep(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteStep", "Could not delete step '{0}'.\nError: {1}",
@@ -318,7 +329,8 @@ export class DeleteAlertAction extends Action {
constructor(
@INotificationService private _notificationService: INotificationService,
@IJobManagementService private _jobService: IJobManagementService
@IJobManagementService private _jobService: IJobManagementService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
super(DeleteAlertAction.ID, DeleteAlertAction.LABEL);
}
@@ -332,6 +344,7 @@ export class DeleteAlertAction extends Action {
[{
label: DeleteAlertAction.LABEL,
run: () => {
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentAlert);
self._jobService.deleteAlert(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteAlert", "Could not delete alert '{0}'.\nError: {1}",
@@ -397,7 +410,8 @@ export class DeleteOperatorAction extends Action {
constructor(
@INotificationService private _notificationService: INotificationService,
@IJobManagementService private _jobService: IJobManagementService
@IJobManagementService private _jobService: IJobManagementService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
super(DeleteOperatorAction.ID, DeleteOperatorAction.LABEL);
}
@@ -411,6 +425,7 @@ export class DeleteOperatorAction extends Action {
[{
label: DeleteOperatorAction.LABEL,
run: () => {
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentOperator);
self._jobService.deleteOperator(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteOperator", "Could not delete operator '{0}'.\nError: {1}",
@@ -477,7 +492,8 @@ export class DeleteProxyAction extends Action {
constructor(
@INotificationService private _notificationService: INotificationService,
@IJobManagementService private _jobService: IJobManagementService
@IJobManagementService private _jobService: IJobManagementService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
super(DeleteProxyAction.ID, DeleteProxyAction.LABEL);
}
@@ -491,6 +507,7 @@ export class DeleteProxyAction extends Action {
[{
label: DeleteProxyAction.LABEL,
run: () => {
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentProxy);
self._jobService.deleteProxy(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteProxy", "Could not delete proxy '{0}'.\nError: {1}",

View File

@@ -77,7 +77,7 @@
<!-- Job History details -->
<div class='history-details'>
<!-- Previous run list -->
<div class="prev-run-list-container" style="min-width: 275px; height: 75vh">
<div class="prev-run-list-container" style="min-width: 250px">
<table *ngIf="_showPreviousRuns === true">
<tr>
<td class="date-column">
@@ -89,7 +89,9 @@
</tr>
</table>
<h3 *ngIf="_showPreviousRuns === false" style="text-align: center">No Previous Runs Available</h3>
<div #table class="step-table prev-run-list" style="position: relative; height: 100%; width: 100%"></div>
<div class="step-table prev-run-list" style="position: relative; width: 100%">
<div #table style="position: absolute; width: 100%; height: 100%"></div>
</div>
</div>
<!-- Job Steps -->
<div class="job-steps" id="job-steps">
@@ -154,8 +156,8 @@
</td>
</tr>
</table>
<div #jobsteps style="height: 100%">
<jobstepsview-component *ngIf="showSteps === true"></jobstepsview-component>
<div #jobsteps style="flex: 1 1 auto; position: relative">
<jobstepsview-component *ngIf="showSteps === true" style="position: absolute; height: 100%; width: 100%"></jobstepsview-component>
</div>
<h3 *ngIf="showSteps === false">No Steps Available</h3>
</div>

View File

@@ -23,13 +23,14 @@ import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/work
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { JobManagementView } from 'sql/parts/jobManagement/views/jobManagementView';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { IDashboardService } from 'sql/services/dashboard/common/dashboardService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
export const DASHBOARD_SELECTOR: string = 'jobhistory-component';
@@ -77,7 +78,8 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
@Inject(IContextMenuService) private contextMenuService: IContextMenuService,
@Inject(IJobManagementService) private _jobManagementService: IJobManagementService,
@Inject(IKeybindingService) keybindingService: IKeybindingService,
@Inject(IDashboardService) dashboardService: IDashboardService
@Inject(IDashboardService) dashboardService: IDashboardService,
@Inject(ITelemetryService) private _telemetryService: ITelemetryService
) {
super(commonService, dashboardService, contextMenuService, keybindingService, instantiationService);
this._treeController = new JobHistoryController();
@@ -141,9 +143,9 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
renderer: this._treeRenderer
}, {verticalScrollMode: ScrollbarVisibility.Visible});
this._register(attachListStyler(this._tree, this.themeService));
this._tree.layout(JobHistoryComponent.INITIAL_TREE_HEIGHT);
this._tree.layout(dom.getContentHeight(this._tableContainer.nativeElement));
this.initActionBar();
this._telemetryService.publicLog(TelemetryKeys.JobHistoryView);
}
private loadHistory() {
@@ -293,6 +295,7 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
if (historyDetails && statusBar) {
let historyBottom = historyDetails.getBoundingClientRect().bottom;
let statusTop = statusBar.getBoundingClientRect().top;
let height: number = statusTop - historyBottom - JobHistoryComponent.HEADING_HEIGHT;
if (this._table) {
@@ -302,14 +305,7 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
}
if (this._tree) {
this._tree.layout(height);
}
if (this._jobStepsView) {
let element = this._jobStepsView.nativeElement as HTMLElement;
if (element) {
element.style.height = height + 'px';
}
this._tree.layout(dom.getContentHeight(this._tableContainer.nativeElement));
}
}
}

View File

@@ -177,17 +177,17 @@ table.step-list tr.step-row td {
}
.history-details {
height: 100%;
flex: 1 1 auto;
display: flex;
}
.history-details > .job-steps {
display: block;
flex: 1 1 auto;
display: flex;
border-left: 3px solid #f4f4f4;
padding-left: 10px;
height: 100%;
width: 90%;
overflow-y: scroll;
flex-direction: column;
width: 100%;
}
.vs-dark .history-details > .job-steps {
@@ -241,13 +241,22 @@ table.step-list tr.step-row td {
width: 140px;
}
.steps-tree .monaco-tree .monaco-tree-row {
white-space: normal;
min-height: 40px !important;
.step-table {
flex: 1 1 auto;
}
jobhistory-component .jobhistory-heading-container {
display: -webkit-box;
.prev-run-list-container {
display: flex;
flex-direction: column;
}
jobhistory-component {
display: flex;
flex-direction: column;
}
jobhistory-component > .jobhistory-heading-container {
display: flex;
}
jobhistory-component > .jobhistory-heading-container > .icon.in-progress {
@@ -267,4 +276,4 @@ jobhistory-component > .agent-actionbar-container .monaco-action-bar > ul.action
.hc-black jobhistory-component > .agent-actionbar-container .monaco-action-bar > ul.actions-container {
border-top: 3px solid #2b56f2;
}
}

View File

@@ -22,4 +22,6 @@
</td>
</tr>
</table>
<div class='steps-tree' #table style="height: 100%; width: 100%"></div>
<div class='steps-tree' style="flex: 1 1 auto; position: relative">
<div #table style="position: absolute; height: 100%; width: 100%" ></div>
</div>

View File

@@ -5,6 +5,7 @@
import 'vs/css!./jobStepsView';
import * as dom from 'vs/base/browser/dom';
import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, Injectable, AfterContentChecked } from '@angular/core';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
@@ -20,7 +21,8 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { IJobManagementService } from 'sql/parts/jobManagement/common/interfaces';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
export const JOBSTEPSVIEW_SELECTOR: string = 'jobstepsview-component';
@@ -36,7 +38,6 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit,
private _treeDataSource = new JobStepsViewDataSource();
private _treeRenderer = new JobStepsViewRenderer();
private _treeFilter = new JobStepsViewFilter();
private _pageSize = 1024;
@ViewChild('table') private _tableContainer: ElementRef;
@@ -49,7 +50,8 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit,
@Inject(IInstantiationService) instantiationService: IInstantiationService,
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
@Inject(IKeybindingService) keybindingService: IKeybindingService,
@Inject(IDashboardService) dashboardService: IDashboardService
@Inject(IDashboardService) dashboardService: IDashboardService,
@Inject(ITelemetryService) private _telemetryService: ITelemetryService
) {
super(commonService, dashboardService, contextMenuService, keybindingService, instantiationService);
}
@@ -57,17 +59,8 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit,
ngAfterContentChecked() {
if (this._jobHistoryComponent.stepRows.length > 0) {
this._treeDataSource.data = this._jobHistoryComponent.stepRows;
if (!this._tree) {
this._tree = new Tree(this._tableContainer.nativeElement, {
controller: this._treeController,
dataSource: this._treeDataSource,
filter: this._treeFilter,
renderer: this._treeRenderer
}, { verticalScrollMode: ScrollbarVisibility.Visible });
this._register(attachListStyler(this._tree, this.themeService));
}
this._tree.layout(this._pageSize);
this._tree.setInput(new JobStepsViewModel());
this.layout();
$('jobstepsview-component .steps-tree .monaco-tree').attr('tabIndex', '-1');
$('jobstepsview-component .steps-tree .monaco-tree-row').attr('tabIndex', '0');
}
@@ -79,14 +72,20 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit,
dataSource: this._treeDataSource,
filter: this._treeFilter,
renderer: this._treeRenderer
}, {verticalScrollMode: ScrollbarVisibility.Visible});
}, {verticalScrollMode: ScrollbarVisibility.Visible, horizontalScrollMode: ScrollbarVisibility.Visible });
this.layout();
this._register(attachListStyler(this._tree, this.themeService));
this._telemetryService.publicLog(TelemetryKeys.JobStepsView);
}
public onFirstVisible() {
}
public layout() {
if (this._tree) {
let treeheight = dom.getContentHeight(this._tableContainer.nativeElement);
this._tree.layout(treeheight);
}
}
}

View File

@@ -78,5 +78,6 @@
}
jobstepsview-component {
padding-top: 10px;
}
display: flex;
flex-direction: column;
}

View File

@@ -5,6 +5,7 @@
import * as DOM from 'vs/base/browser/dom';
import { $ } from 'vs/base/browser/builder';
import * as tree from 'vs/base/parts/tree/browser/tree';
import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults';
import { Promise, TPromise } from 'vs/base/common/winjs.base';
@@ -86,7 +87,7 @@ export class JobStepsViewRenderer implements tree.IRenderer {
private _statusIcon: HTMLElement;
public getHeight(tree: tree.ITree, element: JobStepsViewRow): number {
return 22 * Math.ceil(element.message.length/JobManagementUtilities.jobMessageLength);
return 40;
}
public getTemplateId(tree: tree.ITree, element: JobStepsViewRow | JobStepsViewModel): string {
@@ -118,6 +119,7 @@ export class JobStepsViewRenderer implements tree.IRenderer {
let stepMessageCol: HTMLElement = DOM.$('div');
stepMessageCol.className = 'tree-message-col';
stepMessageCol.innerText = element.message;
$(templateData.label).empty();
templateData.label.appendChild(stepIdCol);
templateData.label.appendChild(stepNameCol);
templateData.label.appendChild(stepMessageCol);

View File

@@ -37,6 +37,8 @@ import { IDashboardService } from 'sql/services/dashboard/common/dashboardServic
import { escape } from 'sql/base/common/strings';
import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { tableBackground, cellBackground, cellBorderColor } from 'sql/common/theme/colors';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/common/telemetryKeys';
export const JOBSVIEW_SELECTOR: string = 'jobsview-component';
export const ROW_HEIGHT: number = 45;
@@ -106,7 +108,8 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
@Inject(IInstantiationService) instantiationService: IInstantiationService,
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
@Inject(IKeybindingService) keybindingService: IKeybindingService,
@Inject(IDashboardService) _dashboardService: IDashboardService
@Inject(IDashboardService) _dashboardService: IDashboardService,
@Inject(ITelemetryService) private _telemetryService: ITelemetryService
) {
super(commonService, _dashboardService, contextMenuService, keybindingService, instantiationService);
this._didTabChange = false;
@@ -127,6 +130,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
this._visibilityElement = this._gridEl;
this._parentComponent = this._agentViewComponent;
this._register(this._themeService.onDidColorThemeChange(e => this.updateTheme(e)));
this._telemetryService.publicLog(TelemetryKeys.JobsView);
}
ngOnDestroy() {
@@ -933,19 +937,19 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
// add steps
if (this.jobSteps && this.jobSteps[jobId]) {
let steps = this.jobSteps[jobId];
job[0].JobSteps = steps;
job[0].jobSteps = steps;
}
// add schedules
if (this.jobSchedules && this.jobSchedules[jobId]) {
let schedules = this.jobSchedules[jobId];
job[0].JobSchedules = schedules;
job[0].jobSchedules = schedules;
}
// add alerts
if (this.jobAlerts && this.jobAlerts[jobId]) {
let alerts = this.jobAlerts[jobId];
job[0].Alerts = alerts;
job[0].alerts = alerts;
}
return job && job.length > 0 ? job[0] : undefined;
}

View File

@@ -119,7 +119,20 @@ export class QueryTextEditor extends BaseTextEditor {
if (!this._config) {
this._config = new Configuration(undefined, editorWidget.getDomNode());
}
let editorHeightUsingLines = this._config.editor.lineHeight * editorWidget.getModel().getLineCount();
let editorWidgetModel = editorWidget.getModel();
let lineCount = editorWidgetModel.getLineCount();
// Need to also keep track of lines that wrap; if we just keep into account line count, then the editor's height would not be
// tall enough and we would need to show a scrollbar. Unfortunately, it looks like there isn't any metadata saved in a ICodeEditor
// around max column length for an editor (which we could leverage to see if we need to loop through every line to determine
// number of lines that wrap). Finally, viewportColumn is calculated on editor resizing automatically; we can use it to ensure
// that the viewportColumn will always be greater than any character's column in an editor.
let numberWrappedLines = 0;
for (let line = 1; line <= lineCount; line++) {
if (editorWidgetModel.getLineMaxColumn(line) >= this._config.editor.layoutInfo.viewportColumn - 1) {
numberWrappedLines += Math.ceil(editorWidgetModel.getLineMaxColumn(line) / this._config.editor.layoutInfo.viewportColumn);
}
}
let editorHeightUsingLines = this._config.editor.lineHeight * (lineCount + numberWrappedLines);
let editorHeightUsingMinHeight = Math.max(editorHeightUsingLines, this._minHeight);
this.setHeight(editorHeightUsingMinHeight);
}

View File

@@ -82,9 +82,7 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
if (propName === 'activeCellId') {
let changedProp = changes[propName];
this._activeCellId = changedProp.currentValue;
if (this._activeCellId) {
this.toggleEditMode(false);
}
this.toggleEditMode(false);
break;
}
}
@@ -117,7 +115,7 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
}
return content;
}
// Todo: implement layout
public layout() {

View File

@@ -35,7 +35,7 @@ export class CellModel implements ICellModel {
private _active: boolean;
private _cellUri: URI;
constructor(private factory: IModelFactory, cellData?: nb.ICell, private _options?: ICellModelOptions) {
constructor(private factory: IModelFactory, cellData?: nb.ICellContents, private _options?: ICellModelOptions) {
this.id = `${modelId++}`;
CellModel.CreateLanguageMappings();
// Do nothing for now
@@ -263,8 +263,8 @@ export class CellModel implements ICellModel {
return transient['display_id'] as string;
}
public toJSON(): nb.ICell {
let cellJson: Partial<nb.ICell> = {
public toJSON(): nb.ICellContents {
let cellJson: Partial<nb.ICellContents> = {
cell_type: this._cellType,
source: this._source,
metadata: {
@@ -275,10 +275,10 @@ export class CellModel implements ICellModel {
cellJson.outputs = this._outputs;
cellJson.execution_count = 1; // TODO: keep track of actual execution count
}
return cellJson as nb.ICell;
return cellJson as nb.ICellContents;
}
public fromJSON(cell: nb.ICell): void {
public fromJSON(cell: nb.ICellContents): void {
if (!cell) {
return;
}

View File

@@ -13,7 +13,7 @@ import { ClientSession } from './clientSession';
export class ModelFactory implements IModelFactory {
public createCell(cell: nb.ICell, options: ICellModelOptions): ICellModel {
public createCell(cell: nb.ICellContents, options: ICellModelOptions): ICellModel {
return new CellModel(this, cell, options);
}

View File

@@ -19,6 +19,7 @@ import { INotebookManager } from 'sql/services/notebook/notebookService';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection';
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
export interface IClientSessionOptions {
notebookUri: URI;
@@ -328,6 +329,14 @@ export interface INotebookModel {
* Notifies the notebook of a change in the cell
*/
onCellChange(cell: ICellModel, change: NotebookChangeType): void;
/**
* Push edit operations, basically editing the model. This is the preferred way of
* editing the model. Long-term, this will ensure edit operations can be added to the undo stack
* @param edits The edit operations to perform
*/
pushEditOperations(edits: ISingleNotebookEditOperation[]): void;
}
export interface ICellModelOptions {
@@ -348,7 +357,7 @@ export interface ICellModel {
readonly onOutputsChanged: Event<ReadonlyArray<nb.ICellOutput>>;
setFuture(future: FutureInternal): void;
equals(cellModel: ICellModel): boolean;
toJSON(): nb.ICell;
toJSON(): nb.ICellContents;
}
export interface FutureInternal extends nb.IFuture {
@@ -357,7 +366,7 @@ export interface FutureInternal extends nb.IFuture {
export interface IModelFactory {
createCell(cell: nb.ICell, options: ICellModelOptions): ICellModel;
createCell(cell: nb.ICellContents, options: ICellModelOptions): ICellModel;
createClientSession(options: IClientSessionOptions): IClientSession;
}

View File

@@ -22,6 +22,7 @@ import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection';
import { INotification, Severity } from 'vs/platform/notification/common/notification';
import { Schemas } from 'vs/base/common/network';
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
/*
* Used to control whether a message in a dialog/wizard is displayed as an error,
@@ -237,7 +238,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
}
private createCell(cellType: CellType): ICellModel {
let singleCell: nb.ICell = {
let singleCell: nb.ICellContents = {
cell_type: cellType,
source: '',
metadata: {},
@@ -263,6 +264,25 @@ export class NotebookModel extends Disposable implements INotebookModel {
}
}
pushEditOperations(edits: ISingleNotebookEditOperation[]): void {
if (this.inErrorState || !this._cells) {
return;
}
for (let edit of edits) {
let newCells: ICellModel[] = [];
if (edit.cell) {
// TODO: should we validate and complete required missing parameters?
let contents: nb.ICellContents = edit.cell as nb.ICellContents;
newCells.push(this.notebookOptions.factory.createCell(contents, { notebook: this, isTrusted: this._trustedMode }));
}
this._cells.splice(edit.range.start, edit.range.end - edit.range.start, ...newCells);
this._contentChangedEmitter.fire({
changeType: NotebookChangeType.CellsAdded
});
}
}
public get activeCell(): ICellModel {
return this._activeCell;
}
@@ -281,9 +301,14 @@ export class NotebookModel extends Disposable implements INotebookModel {
notebookManager: this.notebookManager,
notificationService: this.notebookOptions.notificationService
});
let id: string = this.connectionProfile ? this.connectionProfile.id : undefined;
let profile = this.connectionProfile as IConnectionProfile;
if (this.isValidKnoxConnection(profile)) {
this._hadoopConnection = new NotebookConnection(this.connectionProfile);
} else {
this._hadoopConnection = undefined;
}
this._hadoopConnection = this.connectionProfile ? new NotebookConnection(this.connectionProfile) : undefined;
this._clientSession.initialize(this._hadoopConnection);
this._sessionLoadFinished = this._clientSession.ready.then(async () => {
if (this._clientSession.isInErrorState) {
@@ -389,7 +414,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
// Get default language if saved in notebook file
// Otherwise, default to python
private getDefaultLanguageInfo(notebook: nb.INotebook): nb.ILanguageInfo {
private getDefaultLanguageInfo(notebook: nb.INotebookContents): nb.ILanguageInfo {
return notebook!.metadata!.language_info || {
name: 'python',
version: '',
@@ -398,7 +423,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
}
// Get default kernel info if saved in notebook file
private getSavedKernelInfo(notebook: nb.INotebook): nb.IKernelInfo {
private getSavedKernelInfo(notebook: nb.INotebookContents): nb.IKernelInfo {
return notebook!.metadata!.kernelspec;
}
@@ -490,8 +515,8 @@ export class NotebookModel extends Disposable implements INotebookModel {
/**
* Serialize the model to JSON.
*/
toJSON(): nb.INotebook {
let cells: nb.ICell[] = this.cells.map(c => c.toJSON());
toJSON(): nb.INotebookContents {
let cells: nb.ICellContents[] = this.cells.map(c => c.toJSON());
let metadata = Object.create(null) as nb.INotebookMetadata;
// TODO update language and kernel when these change
metadata.kernelspec = this._savedKernelInfo;

View File

@@ -140,9 +140,16 @@ export class SparkMagicContexts {
* @param savedKernelInfo kernel info loaded from
*/
public static getDefaultKernel(specs: nb.IAllKernels, connectionInfo: IConnectionProfile, savedKernelInfo: nb.IKernelInfo, notificationService: INotificationService): nb.IKernelSpec {
let defaultKernel = specs.kernels.find((kernel) => kernel.name === specs.defaultKernel);
let foundSavedKernelInSpecs;
let defaultKernel;
if (specs) {
defaultKernel = specs.kernels.find((kernel) => kernel.name === specs.defaultKernel);
if (savedKernelInfo) {
foundSavedKernelInSpecs = specs.kernels.find((kernel) => kernel.name === savedKernelInfo.name);
}
}
let profile = connectionInfo as IConnectionProfile;
if (specs && connectionInfo && profile.providerName === notebookConstants.hadoopKnoxProviderName) {
if (foundSavedKernelInSpecs && specs && connectionInfo && profile.providerName === notebookConstants.hadoopKnoxProviderName) {
// set default kernel to default spark kernel if profile exists
// otherwise, set default to kernel info loaded from existing file
defaultKernel = !savedKernelInfo ? specs.kernels.find((spec) => spec.name === notebookConstants.defaultSparkKernel) : savedKernelInfo;

View File

@@ -7,9 +7,9 @@
<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; height: 36px">
</div>
<div class="scrollable" style="flex: 1 1 auto; position: relative">
<div class="scrollable" style="flex: 1 1 auto; position: relative" (click)="unselectActiveCell()">
<loading-spinner [loading]="isLoading"></loading-spinner>
<div class="notebook-cell" *ngFor="let cell of cells" (click)="selectCell(cell)" [class.active]="cell.active" (keydown)="onKeyDown($event)">
<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">

View File

@@ -7,7 +7,7 @@ import './notebookStyles';
import { nb } from 'sqlops';
import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild } from '@angular/core';
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';
@@ -20,7 +20,7 @@ import { AngularDisposable } from 'sql/base/common/lifecycle';
import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts';
import { ICellModel, IModelFactory, notebookConstants } from 'sql/parts/notebook/models/modelInterfaces';
import { IConnectionManagementService, IConnectionDialogService } from 'sql/parts/connection/common/connectionManagement';
import { INotebookService, INotebookParams, INotebookManager } from 'sql/services/notebook/notebookService';
import { INotebookService, INotebookParams, INotebookManager, INotebookEditor } from 'sql/services/notebook/notebookService';
import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService';
import { NotebookModel, NotebookContentChange } from 'sql/parts/notebook/models/notebookModel';
import { ModelFactory } from 'sql/parts/notebook/models/modelFactory';
@@ -40,6 +40,7 @@ import { fillInActions, LabeledMenuItemActionItem } from 'vs/platform/actions/br
import { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
export const NOTEBOOK_SELECTOR: string = 'notebook-component';
@@ -48,7 +49,7 @@ export const NOTEBOOK_SELECTOR: string = 'notebook-component';
selector: NOTEBOOK_SELECTOR,
templateUrl: decodeURI(require.toUrl('./notebook.component.html'))
})
export class NotebookComponent extends AngularDisposable implements OnInit {
export class NotebookComponent extends AngularDisposable implements OnInit, OnDestroy, INotebookEditor {
@ViewChild('toolbar', { read: ElementRef }) private toolbar: ElementRef;
private _model: NotebookModel;
private _isInErrorState: boolean = false;
@@ -73,7 +74,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
@Inject(IEditorService) private editorService: IEditorService,
@Inject(INotificationService) private notificationService: INotificationService,
@Inject(INotebookService) private notebookService: INotebookService,
@Inject(IBootstrapParams) private notebookParams: INotebookParams,
@Inject(IBootstrapParams) private _notebookParams: INotebookParams,
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
@Inject(IContextMenuService) private contextMenuService: IContextMenuService,
@Inject(IContextViewService) private contextViewService: IContextViewService,
@@ -108,10 +109,17 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
ngOnInit() {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
this.notebookService.addNotebookEditor(this);
this.initActionBar();
this.doLoad();
}
ngOnDestroy() {
if (this.notebookService) {
this.notebookService.removeNotebookEditor(this);
}
}
public get model(): NotebookModel {
return this._model;
}
@@ -133,7 +141,10 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
toolbarEl.style.borderBottomColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
}
public selectCell(cell: ICellModel) {
public selectCell(cell: ICellModel, event?: Event) {
if (event) {
event.stopPropagation();
}
if (cell !== this._activeCell) {
if (this._activeCell) {
this._activeCell.active = false;
@@ -146,6 +157,16 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
}
}
public unselectActiveCell() {
if (this._activeCell) {
this._activeCell.active = false;
}
this._activeCell = null;
this._model.activeCell = null;
this._activeCellId = null;
this._changeRef.detectChanges();
}
// Add cell based on cell type
public addCell(cellType: CellType)
{
@@ -201,16 +222,16 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
}
private async loadModel(): Promise<void> {
this.notebookManager = await this.notebookService.getOrCreateNotebookManager(this.notebookParams.providerId, this.notebookParams.notebookUri);
this.notebookManager = await this.notebookService.getOrCreateNotebookManager(this._notebookParams.providerId, this._notebookParams.notebookUri);
let model = new NotebookModel({
factory: this.modelFactory,
notebookUri: this.notebookParams.notebookUri,
notebookUri: this._notebookParams.notebookUri,
connectionService: this.connectionManagementService,
notificationService: this.notificationService,
notebookManager: this.notebookManager
}, false, this.profile);
model.onError((errInfo: INotification) => this.handleModelError(errInfo));
await model.requestModelLoad(this.notebookParams.isTrusted);
await model.requestModelLoad(this._notebookParams.isTrusted);
model.contentChanged((change) => this.handleContentChanged(change));
this._model = model;
this.updateToolbarComponents(this._model.trustedMode);
@@ -231,10 +252,10 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
}
private get modelFactory(): IModelFactory {
if (!this.notebookParams.modelFactory) {
this.notebookParams.modelFactory = new ModelFactory();
if (!this._notebookParams.modelFactory) {
this._notebookParams.modelFactory = new ModelFactory();
}
return this.notebookParams.modelFactory;
return this._notebookParams.modelFactory;
}
private handleModelError(notification: INotification): void {
this.notificationService.notify(notification);
@@ -314,6 +335,9 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
public async save(): Promise<boolean> {
try {
let saved = await this._model.saveModel();
if (saved) {
this.setDirty(false);
}
return saved;
} catch (err) {
this.notificationService.error(localize('saveFailed', 'Failed to save notebook: {0}', notebookUtils.getErrorMessage(err)));
@@ -322,10 +346,9 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
}
private setDirty(isDirty: boolean): void {
// TODO reenable handling of isDirty
// if (this.editor) {
// this.editor.isDirty = isDirty;
// }
if(this._notebookParams.input){
this._notebookParams.input.setDirty(isDirty);
}
}
private actionItemProvider(action: Action): IActionItem {
@@ -337,4 +360,32 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
return undefined;
}
public get notebookParams(): INotebookParams {
return this._notebookParams;
}
public get id(): string {
return this._notebookParams.notebookUri.toString();
}
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;
}
}

View File

@@ -4,50 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import { Registry } from 'vs/platform/registry/common/platform';
import { EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
import { IConfigurationRegistry, Extensions as ConfigExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { Action } from 'vs/base/common/actions';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { TPromise } from 'vs/base/common/winjs.base';
import { Schemas } from 'vs/base/common/network';
import URI from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { NotebookInput, NotebookInputModel, notebooksEnabledCondition } from 'sql/parts/notebook/notebookInput';
import { NotebookInput } from 'sql/parts/notebook/notebookInput';
import { NotebookEditor } from 'sql/parts/notebook/notebookEditor';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
let counter = 0;
/**
* todo: Will remove this code.
* This is the entry point to open the new Notebook
*/
export class NewNotebookAction extends Action {
public static ID = 'workbench.action.newnotebook';
public static LABEL = localize('workbench.action.newnotebook.description', 'New Notebook');
constructor(
id: string,
label: string,
@IEditorService private _editorService: IEditorService,
@IInstantiationService private _instantiationService: IInstantiationService
) {
super(id, label);
}
public run(): TPromise<void> {
let title = `Untitled-${counter++}`;
let untitledUri = URI.from({ scheme: Schemas.untitled, path: title });
let model = new NotebookInputModel(untitledUri, undefined, false, undefined);
let input = this._instantiationService.createInstance(NotebookInput, title, model);
return this._editorService.openEditor(input, { pinned: true }).then(() => undefined);
}
}
// Model View editor registration
const viewModelEditorDescriptor = new EditorDescriptor(
@@ -58,31 +18,3 @@ const viewModelEditorDescriptor = new EditorDescriptor(
Registry.as<IEditorRegistry>(EditorExtensions.Editors)
.registerEditor(viewModelEditorDescriptor, [new SyncDescriptor(NotebookInput)]);
// Feature flag for built-in Notebooks. Will be removed in the future.
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': 'notebook',
'title': 'Notebook',
'type': 'object',
'properties': {
'notebook.enabled': {
'type': 'boolean',
'default': false,
'description': localize('notebook.enabledDescription', 'Enable viewing notebook files using built-in notebook editor.')
}
}
});
// this is the entry point to open the new Notebook
CommandsRegistry.registerCommand(NewNotebookAction.ID, serviceAccessor => {
serviceAccessor.get(IInstantiationService).createInstance(NewNotebookAction, NewNotebookAction.ID, NewNotebookAction.LABEL).run();
});
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: NewNotebookAction.ID,
title:NewNotebookAction.LABEL,
},
when: notebooksEnabledCondition
});

View File

@@ -36,6 +36,10 @@
font-size: 13px;
}
.notebookEditor .monaco-select-box {
min-width: 150px;
}
.notebookEditor .notebook-button.icon-add{
background-image: url("./media/light/add.svg");
}

View File

@@ -18,6 +18,7 @@ import { NotebookComponent } from 'sql/parts/notebook/notebook.component';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { IConnectionManagementService, IConnectionDialogService } from 'sql/parts/connection/common/connectionManagement';
import { getErrorMessage } from 'sql/parts/notebook/notebookUtils';
import { noKernel } from 'sql/services/notebook/sessionManager';
const msgLoading = localize('loading', 'Loading kernels...');
const kernelLabel: string = localize('Kernel', 'Kernel: ');
@@ -238,7 +239,7 @@ export class AttachToDropdown extends SelectBox {
// Load "Attach To" dropdown with the values corresponding to Kernel dropdown
public async loadAttachToDropdown(model: INotebookModel, currentKernel: string): Promise<void> {
if (currentKernel === notebookConstants.python3) {
if (currentKernel === notebookConstants.python3 || currentKernel === noKernel) {
this.setOptions([msgLocalHost]);
}
else {
@@ -314,6 +315,7 @@ export class AttachToDropdown extends SelectBox {
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;

View File

@@ -86,6 +86,7 @@ export class NotebookEditor extends BaseEditor {
input.hasBootstrapped = true;
let params: INotebookParams = {
notebookUri: input.notebookUri,
input: input,
providerId: input.providerId ? input.providerId : DEFAULT_NOTEBOOK_PROVIDER,
isTrusted: input.isTrusted
};

View File

@@ -11,6 +11,7 @@ import { EditorInput, EditorModel, ConfirmResult } from 'vs/workbench/common/edi
import { Emitter, Event } from 'vs/base/common/event';
import URI from 'vs/base/common/uri';
import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import * as resources from 'vs/base/common/resources';
import { INotebookService } from 'sql/services/notebook/notebookService';
@@ -88,15 +89,6 @@ export class NotebookInput extends EditorInput {
) {
super();
this._model.onDidChangeDirty(() => this._onDidChangeDirty.fire());
this.onDispose(() => {
if (this.notebookService) {
this.notebookService.handleNotebookClosed(this.notebookUri);
}
});
}
public get title(): string {
return this._title;
}
public get notebookUri(): URI {
@@ -116,6 +108,10 @@ export class NotebookInput extends EditorInput {
}
public getName(): string {
if (!this._title) {
this._title = resources.basenameOrAuthority(this._model.notebookUri);
}
return this._title;
}
@@ -173,4 +169,12 @@ export class NotebookInput extends EditorInput {
save(): TPromise<boolean> {
return this._model.save();
}
/**
* Sets active editor with dirty value.
* @param isDirty boolean value to set editor dirty
*/
setDirty(isDirty: boolean): void {
this._model.setDirty(isDirty);
}
}

View File

@@ -16,7 +16,8 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
collector.addRule(`
.notebookEditor .notebook-cell.active {
border-color: ${activeBorder};
border-width: 2px;
border-width: 1px;
box-shadow: 0px 4px 6px 0px rgba(0,0,0,0.14);
}
`);
}

View File

@@ -5,11 +5,15 @@
'use strict';
import * as path from 'path';
import { nb } from 'sqlops';
import * as os from 'os';
import * as pfs from 'vs/base/node/pfs';
import { localize } from 'vs/nls';
import { IOutputChannel } from 'vs/workbench/parts/output/common/output';
import { Registry } from 'vs/platform/registry/common/platform';
import { INotebookProviderRegistry, Extensions } from 'sql/services/notebook/notebookRegistry';
import { DEFAULT_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_FILETYPE } from 'sql/services/notebook/notebookService';
/**
@@ -36,3 +40,23 @@ export async function mkDir(dirPath: string, outputChannel?: IOutputChannel): Pr
await pfs.mkdirp(dirPath);
}
}
export function getProviderForFileName(fileName: string): string {
let fileExt = path.extname(fileName);
let provider: string;
let notebookRegistry = Registry.as<INotebookProviderRegistry>(Extensions.NotebookProviderContribution);
// First try to get provider for actual file type
if (fileExt && fileExt.startsWith('.')) {
fileExt = fileExt.slice(1,fileExt.length);
provider = notebookRegistry.getProviderForFileType(fileExt);
}
// Fallback to provider for default file type (assume this is a global handler)
if (!provider) {
provider = notebookRegistry.getProviderForFileType(DEFAULT_NOTEBOOK_FILETYPE);
}
// Finally if all else fails, use the built-in handler
if (!provider) {
provider = DEFAULT_NOTEBOOK_PROVIDER;
}
return provider;
}

View File

@@ -9,7 +9,7 @@ import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiati
import * as nls from 'vs/nls';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { IConnectionManagementService, IConnectionDialogService} from 'sql/parts/connection/common/connectionManagement';
import { IConnectionManagementService, IConnectionDialogService } from 'sql/parts/connection/common/connectionManagement';
import { IObjectExplorerService } from '../../objectExplorer/common/objectExplorerService';
import { ProfilerInput } from 'sql/parts/profiler/editor/profilerInput';
import { TPromise } from 'vs/base/common/winjs.base';
@@ -55,10 +55,10 @@ CommandsRegistry.registerCommand({
let promise;
if (connectionProfile) {
promise = connectionService.connectIfNotConnected(connectionProfile);
promise = connectionService.connectIfNotConnected(connectionProfile, 'connection', true);
} else {
// if still no luck, we will open the Connection dialog and let user connect to a server
promise = connectionDialogService.openDialogAndWait(connectionService, { connectionType: 1, providers: [mssqlProviderName] }).then((profile) => {
promise = connectionDialogService.openDialogAndWait(connectionService, { connectionType: 0, showDashboard: false, providers: [mssqlProviderName] }).then((profile) => {
connectionProfile = profile as ConnectionProfile;
});
}

View File

@@ -308,8 +308,6 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas
// ----- Public
public focusFindInput(): void {
this._findInput.select();
// Edge browser requires focus() in addition to select()
this._findInput.focus();
}

View File

@@ -218,6 +218,7 @@ export class ProfilerTableEditor extends BaseEditor implements IProfilerControll
if (p) {
this._profilerTable.setActiveCell(p.row, p.col);
this._updateFinderMatchState();
this._finder.focusFindInput();
}
});
} else {

View File

@@ -557,6 +557,7 @@ export class ProfilerEditor extends BaseEditor {
}
public focus() {
this._profilerEditorContextKey.set(true);
super.focus();
let savedViewState = this._savedTableViewStates.get(this.input);
if (savedViewState) {

View File

@@ -66,7 +66,8 @@ export class ProfilerInput extends EditorInput implements IProfilerSession {
let searchFn = (val: { [x: string]: string }, exp: string): Array<number> => {
let ret = new Array<number>();
for (let i = 0; i < this._columns.length; i++) {
if (val[this._columns[i]].includes(exp)) {
let colVal = val[this._columns[i]];
if (colVal && colVal.toLocaleLowerCase().includes(exp.toLocaleLowerCase())) {
ret.push(i);
}
}

View File

@@ -282,6 +282,7 @@ export class QueryResultsView extends Disposable {
}
public dispose() {
dispose(this.runnerDisposables);
super.dispose();
}
}

View File

@@ -22,8 +22,9 @@ export class CustomDialogService {
constructor( @IInstantiationService private _instantiationService: IInstantiationService) { }
public showDialog(dialog: Dialog, options?: IModalOptions): void {
let dialogModal = this._instantiationService.createInstance(DialogModal, dialog, 'CustomDialog', options || defaultOptions);
public showDialog(dialog: Dialog, dialogName?: string, options?: IModalOptions): void {
let name = dialogName ? dialogName : 'CustomDialog';
let dialogModal = this._instantiationService.createInstance(DialogModal, dialog, name, options || defaultOptions);
this._dialogModals.set(dialog, dialogModal);
dialogModal.render();
dialogModal.open();

View File

@@ -12,10 +12,9 @@ import * as pfs from 'vs/base/node/pfs';
import URI from 'vs/base/common/uri';
import ContentManager = nb.ContentManager;
import INotebook = nb.INotebook;
export class LocalContentManager implements ContentManager {
public async getNotebookContents(notebookUri: URI): Promise<INotebook> {
public async getNotebookContents(notebookUri: URI): Promise<nb.INotebookContents> {
if (!notebookUri) {
return undefined;
}
@@ -23,10 +22,10 @@ export class LocalContentManager implements ContentManager {
let path = notebookUri.fsPath;
// Note: intentionally letting caller handle exceptions
let notebookFileBuffer = await pfs.readFile(path);
return <INotebook>json.parse(notebookFileBuffer.toString());
return <nb.INotebookContents>json.parse(notebookFileBuffer.toString());
}
public async save(notebookUri: URI, notebook: INotebook): Promise<INotebook> {
public async save(notebookUri: URI, notebook: nb.INotebookContents): Promise<nb.INotebookContents> {
// Convert to JSON with pretty-print functionality
let contents = JSON.stringify(notebook, undefined, ' ');
let path = notebookUri.fsPath;

View File

@@ -6,21 +6,29 @@
'use strict';
import * as sqlops from 'sqlops';
import { Event } from 'vs/base/common/event';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import URI from 'vs/base/common/uri';
import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService';
import { RenderMimeRegistry } from 'sql/parts/notebook/outputs/registry';
import { ModelFactory } from 'sql/parts/notebook/models/modelFactory';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { NotebookInput } from 'sql/parts/notebook/notebookInput';
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
export const SERVICE_ID = 'notebookService';
export const INotebookService = createDecorator<INotebookService>(SERVICE_ID);
export const DEFAULT_NOTEBOOK_PROVIDER = 'builtin';
export const DEFAULT_NOTEBOOK_FILETYPE = 'IPYNB';
export interface INotebookService {
_serviceBrand: any;
onNotebookEditorAdd: Event<INotebookEditor>;
onNotebookEditorRemove: Event<INotebookEditor>;
/**
* Register a metadata provider
*/
@@ -40,7 +48,11 @@ export interface INotebookService {
*/
getOrCreateNotebookManager(providerId: string, uri: URI): Thenable<INotebookManager>;
handleNotebookClosed(uri: URI): void;
addNotebookEditor(editor: INotebookEditor): void;
removeNotebookEditor(editor: INotebookEditor): void;
listNotebookEditors(): INotebookEditor[];
shutdown(): void;
@@ -62,8 +74,19 @@ export interface INotebookManager {
export interface INotebookParams extends IBootstrapParams {
notebookUri: URI;
input: NotebookInput;
providerId: string;
isTrusted: boolean;
profile?: IConnectionProfile;
modelFactory?: ModelFactory;
}
export interface INotebookEditor {
readonly notebookParams: INotebookParams;
readonly id: string;
isDirty(): boolean;
isActive(): boolean;
isVisible(): boolean;
save(): Promise<boolean>;
executeEdits(edits: ISingleNotebookEditOperation[]): boolean;
}

View File

@@ -10,20 +10,26 @@ import { localize } from 'vs/nls';
import URI from 'vs/base/common/uri';
import { Registry } from 'vs/platform/registry/common/platform';
import { INotebookService, INotebookManager, INotebookProvider, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService';
import {
INotebookService, INotebookManager, INotebookProvider, DEFAULT_NOTEBOOK_PROVIDER,
DEFAULT_NOTEBOOK_FILETYPE, INotebookEditor
} from 'sql/services/notebook/notebookService';
import { RenderMimeRegistry } from 'sql/parts/notebook/outputs/registry';
import { standardRendererFactories } from 'sql/parts/notebook/outputs/factories';
import { LocalContentManager } from 'sql/services/notebook/localContentManager';
import { SessionManager } from 'sql/services/notebook/sessionManager';
import { Extensions, INotebookProviderRegistry } from 'sql/services/notebook/notebookRegistry';
import { Emitter, Event } from 'vs/base/common/event';
const DEFAULT_NOTEBOOK_FILETYPE = 'IPYNB';
export class NotebookService implements INotebookService {
_serviceBrand: any;
private _mimeRegistry: RenderMimeRegistry;
private _providers: Map<string, INotebookProvider> = new Map();
private _managers: Map<string, INotebookManager> = new Map();
private _onNotebookEditorAdd = new Emitter<INotebookEditor>();
private _onNotebookEditorRemove = new Emitter<INotebookEditor>();
private _editors = new Map<string, INotebookEditor>();
constructor() {
this.registerDefaultProvider();
@@ -71,8 +77,34 @@ export class NotebookService implements INotebookService {
return manager;
}
handleNotebookClosed(notebookUri: URI): void {
get onNotebookEditorAdd(): Event<INotebookEditor> {
return this._onNotebookEditorAdd.event;
}
get onNotebookEditorRemove(): Event<INotebookEditor> {
return this._onNotebookEditorRemove.event;
}
addNotebookEditor(editor: INotebookEditor): void {
this._editors.set(editor.id, editor);
this._onNotebookEditorAdd.fire(editor);
}
removeNotebookEditor(editor: INotebookEditor): void {
if (this._editors.delete(editor.id)) {
this._onNotebookEditorRemove.fire(editor);
}
// Remove the manager from the tracked list, and let the notebook provider know that it should update its mappings
this.sendNotebookCloseToProvider(editor);
}
listNotebookEditors(): INotebookEditor[] {
let editors = [];
this._editors.forEach(e => editors.push(e));
return editors;
}
private sendNotebookCloseToProvider(editor: INotebookEditor) {
let notebookUri = editor.notebookParams.notebookUri;
let uriString = notebookUri.toString();
let manager = this._managers.get(uriString);
if (manager) {
@@ -82,15 +114,20 @@ export class NotebookService implements INotebookService {
}
}
// PRIVATE HELPERS /////////////////////////////////////////////////////
private doWithProvider<T>(providerId: string, op: (provider: INotebookProvider) => Thenable<T>): Thenable<T> {
// Make sure the provider exists before attempting to retrieve accounts
let provider = this._providers.get(providerId);
let provider: INotebookProvider;
if (this._providers.has(providerId)) {
provider = this._providers.get(providerId);
}
else {
provider = this._providers.get(DEFAULT_NOTEBOOK_PROVIDER);
}
if (!provider) {
return Promise.reject(new Error(localize('notebookServiceNoProvider', 'Notebook provider does not exist'))).then();
}
return op(provider);
}
@@ -103,8 +140,6 @@ export class NotebookService implements INotebookService {
}
return this._mimeRegistry;
}
}
export class BuiltinProvider implements INotebookProvider {

View File

@@ -4,7 +4,7 @@ import { nb } from 'sqlops';
import { localize } from 'vs/nls';
import { FutureInternal } from 'sql/parts/notebook/models/modelInterfaces';
const noKernel: string = localize('noKernel', 'No Kernel');
export const noKernel: string = localize('noKernel', 'No Kernel');
const runNotebookDisabled = localize('runNotebookDisabled', 'Cannot run cells as no kernel has been configured');
let noKernelSpec: nb.IKernelSpec = ({
@@ -24,9 +24,9 @@ export class SessionManager implements nb.SessionManager {
public get specs(): nb.IAllKernels {
let allKernels: nb.IAllKernels = {
defaultKernel: noKernel,
defaultKernel: noKernel,
kernels: [noKernelSpec]
};
};
return allKernels;
}
@@ -40,7 +40,7 @@ export class SessionManager implements nb.SessionManager {
}
}
class EmptySession implements nb.ISession {
export class EmptySession implements nb.ISession {
private _kernel: EmptyKernel;
private _defaultKernelLoaded = false;
@@ -146,7 +146,7 @@ class EmptyKernel implements nb.IKernel {
}
}
class EmptyFuture implements FutureInternal {
export class EmptyFuture implements FutureInternal {
get inProgress(): boolean {

20
src/sql/sqlops.d.ts vendored
View File

@@ -214,6 +214,7 @@ declare module 'sqlops' {
providerName: string;
saveProfile: boolean;
id: string;
azureTenantId?: string;
}
/**
@@ -1299,15 +1300,16 @@ declare module 'sqlops' {
lastRun: string;
nextRun: string;
jobId: string;
EmailLevel: JobCompletionActionCondition;
PageLevel: JobCompletionActionCondition;
EventLogLevel: JobCompletionActionCondition;
DeleteLevel: JobCompletionActionCondition;
OperatorToEmail: string;
OperatorToPage: string;
JobSteps: AgentJobStepInfo[];
JobSchedules: AgentJobScheduleInfo[];
Alerts: AgentAlertInfo[];
startStepId: number;
emailLevel: JobCompletionActionCondition;
pageLevel: JobCompletionActionCondition;
eventLogLevel: JobCompletionActionCondition;
deleteLevel: JobCompletionActionCondition;
operatorToEmail: string;
operatorToPage: string;
jobSteps: AgentJobStepInfo[];
jobSchedules: AgentJobScheduleInfo[];
alerts: AgentAlertInfo[];
}
export interface AgentJobScheduleInfo {

View File

@@ -839,7 +839,7 @@ declare module 'sqlops' {
* Create a dialog with the given title
* @param title The title of the dialog, displayed at the top
*/
export function createDialog(title: string): Dialog;
export function createDialog(title: string, dialogName?: string): Dialog;
/**
* Create a dialog tab which can be included as part of the content of a dialog
@@ -951,6 +951,12 @@ declare module 'sqlops' {
*/
message: DialogMessage;
/**
* Set the dialog name when opening
* the dialog for telemetry
*/
dialogName?: string;
/**
* Register a callback that will be called when the user tries to click done. Only
* one callback can be registered at once, so each registration call will clear
@@ -1368,6 +1374,282 @@ declare module 'sqlops' {
}
export namespace nb {
/**
* All notebook documents currently known to the system.
*
* @readonly
*/
export let notebookDocuments: NotebookDocument[];
/**
* The currently active Notebook editor or `undefined`. The active editor is the one
* that currently has focus or, when none has focus, the one that has changed
* input most recently.
*/
export let activeNotebookEditor: NotebookEditor | undefined;
/**
* The currently visible editors or an empty array.
*/
export let visibleNotebookEditors: NotebookEditor[];
/**
* An event that is emitted when a [notebook document](#NotebookDocument) is opened.
*
* To add an event listener when a visible text document is opened, use the [TextEditor](#TextEditor) events in the
* [window](#window) namespace. Note that:
*
* - The event is emitted before the [document](#NotebookDocument) is updated in the
* [active notebook editor](#nb.activeNotebookEditor)
* - When a [notebook document](#NotebookDocument) is already open (e.g.: open in another visible notebook editor) this event is not emitted
*
*/
export const onDidOpenNotebookDocument: vscode.Event<NotebookDocument>;
/**
* An event that is emitted when a [notebook's](#NotebookDocument) cell contents are changed.
*/
export const onDidChangeNotebookCell: vscode.Event<NotebookCellChangeEvent>;
/**
* Show the given document in a notebook editor. A [column](#ViewColumn) can be provided
* to control where the editor is being shown. Might change the [active editor](#nb.activeNotebookEditor).
*
* The document is denoted by an [uri](#Uri). Depending on the [scheme](#Uri.scheme) the
* following rules apply:
* `file`-scheme: Open a file on disk, will be rejected if the file does not exist or cannot be loaded.
* `untitled`-scheme: A new file that should be saved on disk, e.g. `untitled:c:\frodo\new.js`. The language
* will be derived from the file name.
* For all other schemes the registered notebook providers are consulted.
*
* @param document A document to be shown.
* @param column A view column in which the [editor](#NotebookEditor) should be shown. The default is the [active](#ViewColumn.Active), other values
* are adjusted to be `Min(column, columnCount + 1)`, the [active](#ViewColumn.Active)-column is not adjusted. Use [`ViewColumn.Beside`](#ViewColumn.Beside)
* to open the editor to the side of the currently active one.
* @param preserveFocus When `true` the editor will not take focus.
* @return A promise that resolves to a [notebook editor](#NotebookEditor).
*/
export function showNotebookDocument(uri: vscode.Uri, showOptions?: NotebookShowOptions): Thenable<NotebookEditor>;
export interface NotebookDocument {
/**
* The associated uri for this notebook document.
*
* *Note* that most documents use the `file`-scheme, which means they are files on disk. However, **not** all documents are
* saved on disk and therefore the `scheme` must be checked before trying to access the underlying file or siblings on disk.
*
*/
readonly uri: vscode.Uri;
/**
* The file system path of the associated resource. Shorthand
* notation for [TextDocument.uri.fsPath](#TextDocument.uri). Independent of the uri scheme.
*/
readonly fileName: string;
/**
* Is this document representing an untitled file which has never been saved yet. *Note* that
* this does not mean the document will be saved to disk, use [`uri.scheme`](#Uri.scheme)
* to figure out where a document will be [saved](#FileSystemProvider), e.g. `file`, `ftp` etc.
*/
readonly isUntitled: boolean;
/**
* The identifier of the Notebook provider associated with this document.
*/
readonly providerId: string;
/**
* `true` if there are unpersisted changes.
*/
readonly isDirty: boolean;
/**
* `true` if the document have been closed. A closed document isn't synchronized anymore
* and won't be re-used when the same resource is opened again.
*/
readonly isClosed: boolean;
/**
* All cells.
*/
readonly cells: NotebookCell[];
/**
* Save the underlying file.
*
* @return A promise that will resolve to true when the file
* has been saved. If the file was not dirty or the save failed,
* will return false.
*/
save(): Thenable<boolean>;
/**
* Ensure a cell range is completely contained in this document.
*
* @param range A cell range.
* @return The given range or a new, adjusted range.
*/
validateCellRange(range: CellRange): CellRange;
}
/**
* A cell range represents an ordered pair of two positions in a list of cells.
* It is guaranteed that [start](#CellRange.start).isBeforeOrEqual([end](#CellRange.end))
*
* CellRange objects are __immutable__.
*/
export class CellRange {
/**
* The start index. It is before or equal to [end](#CellRange.end).
*/
readonly start: number;
/**
* The end index. It is after or equal to [start](#CellRange.start).
*/
readonly end: number;
/**
* Create a new range from two positions. If `start` is not
* before or equal to `end`, the values will be swapped.
*
* @param start A number.
* @param end A number.
*/
constructor(start: number, end: number);
}
export interface NotebookEditor {
/**
* The document associated with this editor. The document will be the same for the entire lifetime of this editor.
*/
readonly document: NotebookDocument;
/**
* The column in which this editor shows. Will be `undefined` in case this
* isn't one of the main editors, e.g an embedded editor, or when the editor
* column is larger than three.
*/
viewColumn?: vscode.ViewColumn;
/**
* Perform an edit on the document associated with this notebook editor.
*
* The given callback-function is invoked with an [edit-builder](#NotebookEditorEdit) which must
* be used to make edits. Note that the edit-builder is only valid while the
* callback executes.
*
* @param callback A function which can create edits using an [edit-builder](#NotebookEditorEdit).
* @param options The undo/redo behavior around this edit. By default, undo stops will be created before and after this edit.
* @return A promise that resolves with a value indicating if the edits could be applied.
*/
edit(callback: (editBuilder: NotebookEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable<boolean>;
}
export interface NotebookCell {
contents: ICellContents;
}
export interface NotebookShowOptions {
/**
* An optional view column in which the [editor](#NotebookEditor) should be shown.
* The default is the [active](#ViewColumn.Active), other values are adjusted to
* be `Min(column, columnCount + 1)`, the [active](#ViewColumn.Active)-column is
* not adjusted. Use [`ViewColumn.Beside`](#ViewColumn.Beside) to open the
* editor to the side of the currently active one.
*/
viewColumn?: vscode.ViewColumn;
/**
* An optional flag that when `true` will stop the [editor](#NotebookEditor) from taking focus.
*/
preserveFocus?: boolean;
/**
* An optional flag that controls if an [editor](#NotebookEditor)-tab will be replaced
* with the next editor or if it will be kept.
*/
preview?: boolean;
/**
* An optional string indicating which notebook provider to initially use
*/
providerId?: string;
/**
* Optional ID indicating the initial connection to use for this editor
*/
connectionId?: string;
}
/**
* Represents an event describing the change in a [notebook documents's cells](#NotebookDocument.cells).
*/
export interface NotebookCellChangeEvent {
/**
* The [notebook document](#NotebookDocument) for which the selections have changed.
*/
notebook: NotebookDocument;
/**
* The new value for the [notebook documents's cells](#NotebookDocument.cells).
*/
cell: NotebookCell[];
/**
* The [change kind](#TextEditorSelectionChangeKind) which has triggered this
* event. Can be `undefined`.
*/
kind?: vscode.TextEditorSelectionChangeKind;
}
/**
* A complex edit that will be applied in one transaction on a TextEditor.
* This holds a description of the edits and if the edits are valid (i.e. no overlapping regions, document was not changed in the meantime, etc.)
* they can be applied on a [document](#TextDocument) associated with a [text editor](#TextEditor).
*
*/
export interface NotebookEditorEdit {
/**
* Replace a cell range with a new cell.
*
* @param location The range this operation should remove.
* @param value The new cell this operation should insert after removing `location`.
*/
replace(location: number | CellRange, value: ICellContents): void;
/**
* Insert a cell (optionally) at a specific index. Any index outside of the length of the cells
* will result in the cell being added at the end.
*
* @param index The position where the new text should be inserted.
* @param value The new text this operation should insert.
*/
insertCell(value: ICellContents, index?: number): void;
/**
* Delete a certain cell.
*
* @param index The index of the cell to remove.
*/
deleteCell(index: number): void;
}
/**
* Register a notebook provider. The supported file types handled by this
* provider are defined in the `package.json:
* ```json
* {
* "contributes": {
* "notebook.providers": [{
* "provider": "providername",
* "fileExtensions": ["FILEEXT"]
* }]
* }
* }
* ```
* @export
* @param {NotebookProvider} provider
* @returns {vscode.Disposable}
*/
export function registerNotebookProvider(provider: NotebookProvider): vscode.Disposable;
export interface NotebookProvider {
@@ -1431,7 +1713,7 @@ declare module 'sqlops' {
/* Reads contents from a Uri representing a local or remote notebook and returns a
* JSON object containing the cells and metadata about the notebook
*/
getNotebookContents(notebookUri: vscode.Uri): Thenable<INotebook>;
getNotebookContents(notebookUri: vscode.Uri): Thenable<INotebookContents>;
/**
* Save a file.
@@ -1443,12 +1725,19 @@ declare module 'sqlops' {
* @returns A thenable which resolves with the file content model when the
* file is saved.
*/
save(notebookUri: vscode.Uri, notebook: INotebook): Thenable<INotebook>;
save(notebookUri: vscode.Uri, notebook: INotebookContents): Thenable<INotebookContents>;
}
export interface INotebook {
readonly cells: ICell[];
/**
* Interface defining the file format contents of a notebook, usually in a serializable
* format. This interface does not have any methods for manipulating or interacting
* with a notebook object.
*
*/
export interface INotebookContents {
readonly cells: ICellContents[];
readonly metadata: INotebookMetadata;
readonly nbformat: number;
readonly nbformat_minor: number;
@@ -1477,7 +1766,13 @@ declare module 'sqlops' {
version: string;
}
export interface ICell {
/**
* Interface defining the file format contents of a notebook cell, usually in a serializable
* format. This interface does not have any methods for manipulating or interacting
* with a cell object.
*
*/
export interface ICellContents {
cell_type: CellType;
source: string | string[];
metadata: {

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import { nb } from 'sqlops';
import { TreeItem } from 'vs/workbench/api/node/extHostTypes';
// SQL added extension host types
@@ -457,4 +458,44 @@ export enum FutureMessageType {
export interface INotebookFutureDone {
succeeded: boolean;
rejectReason: string;
}
}
export interface ICellRange {
readonly start: number;
readonly end: number;
}
export class CellRange {
protected _start: number;
protected _end: number;
get start(): number {
return this._start;
}
get end(): number {
return this._end;
}
constructor(start: number, end: number) {
if (typeof(start) !== 'number' || typeof(start) !== 'number' || start < 0 || end < 0) {
throw new Error('Invalid arguments');
}
// Logic taken from range handling.
if (start <= end) {
this._start = start;
this._end = end;
} else {
this._start = end;
this._end = start;
}
}
}
export interface ISingleNotebookEditOperation {
range: ICellRange;
cell: Partial<nb.ICellContents>;
forceMoveMarkers: boolean;
}

View File

@@ -16,6 +16,8 @@ import * as sqlops from 'sqlops';
import { SqlMainContext, ExtHostModelViewDialogShape, MainThreadModelViewDialogShape, ExtHostModelViewShape, ExtHostBackgroundTaskManagementShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { IItemConfig, ModelComponentTypes, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { Inject } from '@angular/core';
const DONE_LABEL = nls.localize('dialogDoneLabel', 'Done');
const CANCEL_LABEL = nls.localize('dialogCancelLabel', 'Cancel');
@@ -125,6 +127,7 @@ class DialogImpl extends ModelViewPanelImpl implements sqlops.window.modelviewdi
private _message: sqlops.window.modelviewdialog.DialogMessage;
private _closeValidator: () => boolean | Thenable<boolean>;
private _operationHandler: BackgroundOperationHandler;
private _dialogName: string;
constructor(extHostModelViewDialog: ExtHostModelViewDialog,
extHostModelView: ExtHostModelViewShape,
@@ -157,6 +160,14 @@ class DialogImpl extends ModelViewPanelImpl implements sqlops.window.modelviewdi
this._extHostModelViewDialog.updateDialogContent(this);
}
public get dialogName(): string {
return this._dialogName;
}
public set dialogName(value: string) {
this._dialogName = value;
}
public registerCloseValidator(validator: () => boolean | Thenable<boolean>): void {
this._closeValidator = validator;
}
@@ -503,7 +514,8 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape {
public openDialog(dialog: sqlops.window.modelviewdialog.Dialog): void {
let handle = this.getHandle(dialog);
this.updateDialogContent(dialog);
this._proxy.$openDialog(handle);
dialog.dialogName ? this._proxy.$openDialog(handle, dialog.dialogName) :
this._proxy.$openDialog(handle);
}
public closeDialog(dialog: sqlops.window.modelviewdialog.Dialog): void {
@@ -560,8 +572,11 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape {
this._onClickCallbacks.set(handle, callback);
}
public createDialog(title: string, extensionLocation?: URI): sqlops.window.modelviewdialog.Dialog {
public createDialog(title: string, dialogName?: string, extensionLocation?: URI): sqlops.window.modelviewdialog.Dialog {
let dialog = new DialogImpl(this, this._extHostModelView, this._extHostTaskManagement, extensionLocation);
if (dialogName) {
dialog.dialogName = dialogName;
}
dialog.title = title;
dialog.handle = this.getHandle(dialog);
return dialog;

View File

@@ -15,6 +15,7 @@ import URI, { UriComponents } from 'vs/base/common/uri';
import { ExtHostNotebookShape, MainThreadNotebookShape, SqlMainContext } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { INotebookManagerDetails, INotebookSessionDetails, INotebookKernelDetails, INotebookFutureDetails, FutureMessageType } from 'sql/workbench/api/common/sqlExtHostTypes';
import { Event, Emitter } from 'vs/base/common/event';
type Adapter = sqlops.nb.NotebookProvider | sqlops.nb.NotebookManager | sqlops.nb.ISession | sqlops.nb.IKernel | sqlops.nb.IFuture;
@@ -23,6 +24,12 @@ export class ExtHostNotebook implements ExtHostNotebookShape {
private readonly _proxy: MainThreadNotebookShape;
private _adapters = new Map<number, Adapter>();
private _onDidOpenNotebook = new Emitter<sqlops.nb.NotebookDocument>();
private _onDidChangeNotebookCell = new Emitter<sqlops.nb.NotebookCellChangeEvent>();
public readonly onDidOpenNotebookDocument: Event<sqlops.nb.NotebookDocument> = this._onDidOpenNotebook.event;
public readonly onDidChangeNotebookCell: Event<sqlops.nb.NotebookCellChangeEvent> = this._onDidChangeNotebookCell.event;
// Notebook URI to manager lookup.
constructor(_mainContext: IMainContext) {
this._proxy = _mainContext.getProxy(SqlMainContext.MainThreadNotebook);
@@ -63,11 +70,11 @@ export class ExtHostNotebook implements ExtHostNotebookShape {
return this._withServerManager(managerHandle, (serverManager) => serverManager.stopServer());
}
$getNotebookContents(managerHandle: number, notebookUri: UriComponents): Thenable<sqlops.nb.INotebook> {
$getNotebookContents(managerHandle: number, notebookUri: UriComponents): Thenable<sqlops.nb.INotebookContents> {
return this._withContentManager(managerHandle, (contentManager) => contentManager.getNotebookContents(URI.revive(notebookUri)));
}
$save(managerHandle: number, notebookUri: UriComponents, notebook: sqlops.nb.INotebook): Thenable<sqlops.nb.INotebook> {
$save(managerHandle: number, notebookUri: UriComponents, notebook: sqlops.nb.INotebookContents): Thenable<sqlops.nb.INotebookContents> {
return this._withContentManager(managerHandle, (contentManager) => contentManager.save(URI.revive(notebookUri), notebook));
}

View File

@@ -0,0 +1,101 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import { IDisposable } from 'vs/base/common/lifecycle';
import URI from 'vs/base/common/uri';
import { ok } from 'vs/base/common/assert';
import { Schemas } from 'vs/base/common/network';
import { TPromise } from 'vs/base/common/winjs.base';
import { MainThreadNotebookDocumentsAndEditorsShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { CellRange } from 'sql/workbench/api/common/sqlExtHostTypes';
export class ExtHostNotebookDocumentData implements IDisposable {
private _document: sqlops.nb.NotebookDocument;
private _cells: sqlops.nb.NotebookCell[];
private _isDisposed: boolean = false;
constructor(private readonly _proxy: MainThreadNotebookDocumentsAndEditorsShape,
private readonly _uri: URI,
private readonly _providerId: string,
private _isDirty: boolean
) {
// TODO add cell mapping support
this._cells = [];
}
dispose(): void {
// we don't really dispose documents but let
// extensions still read from them. some
// operations, live saving, will now error tho
ok(!this._isDisposed);
this._isDisposed = true;
this._isDirty = false;
}
get document(): sqlops.nb.NotebookDocument {
if (!this._document) {
const data = this;
this._document = {
get uri() { return data._uri; },
get fileName() { return data._uri.fsPath; },
get isUntitled() { return data._uri.scheme === Schemas.untitled; },
get providerId() { return data._providerId; },
get isClosed() { return data._isDisposed; },
get isDirty() { return data._isDirty; },
get cells() { return data._cells; },
save() { return data._save(); },
validateCellRange(range) { return data._validateRange(range); },
};
}
return Object.freeze(this._document);
}
private _save(): Thenable<boolean> {
if (this._isDisposed) {
return TPromise.wrapError<boolean>(new Error('Document has been closed'));
}
return this._proxy.$trySaveDocument(this._uri);
}
// ---- range math
private _validateRange(range: sqlops.nb.CellRange): sqlops.nb.CellRange {
if (!(range instanceof CellRange)) {
throw new Error('Invalid argument');
}
let start = this._validateIndex(range.start);
let end = this._validateIndex(range.end);
if (start === range.start && end === range.end) {
return range;
}
return new CellRange(start, end);
}
private _validateIndex(index: number): number {
if (typeof(index) !== 'number') {
throw new Error('Invalid argument');
}
if (index < 0) {
index = 0;
} else if (this._cells.length > 0 && index > this._cells.length) {
// We allow off by 1 as end needs to be outside current length in order to
// handle replace scenario. Long term should consider different start vs end validation instead
index = this._cells.length;
}
return index;
}
}

View File

@@ -0,0 +1,192 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import { Event, Emitter } from 'vs/base/common/event';
import { dispose } from 'vs/base/common/lifecycle';
import URI from 'vs/base/common/uri';
import { Disposable } from 'vs/workbench/api/node/extHostTypes';
import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters';
import { IMainContext } from 'vs/workbench/api/node/extHost.protocol';
import { ok } from 'vs/base/common/assert';
import {
SqlMainContext, INotebookDocumentsAndEditorsDelta, ExtHostNotebookDocumentsAndEditorsShape,
MainThreadNotebookDocumentsAndEditorsShape, INotebookShowOptions
} from 'sql/workbench/api/node/sqlExtHost.protocol';
import { ExtHostNotebookDocumentData } from 'sql/workbench/api/node/extHostNotebookDocumentData';
import { ExtHostNotebookEditor } from 'sql/workbench/api/node/extHostNotebookEditor';
export class ExtHostNotebookDocumentsAndEditors implements ExtHostNotebookDocumentsAndEditorsShape {
private _disposables: Disposable[] = [];
private _activeEditorId: string;
private _proxy: MainThreadNotebookDocumentsAndEditorsShape;
private readonly _editors = new Map<string, ExtHostNotebookEditor>();
private readonly _documents = new Map<string, ExtHostNotebookDocumentData>();
private readonly _onDidAddDocuments = new Emitter<ExtHostNotebookDocumentData[]>();
private readonly _onDidRemoveDocuments = new Emitter<ExtHostNotebookDocumentData[]>();
private readonly _onDidChangeVisibleNotebookEditors = new Emitter<ExtHostNotebookEditor[]>();
private readonly _onDidChangeActiveNotebookEditor = new Emitter<ExtHostNotebookEditor>();
readonly onDidAddDocuments: Event<ExtHostNotebookDocumentData[]> = this._onDidAddDocuments.event;
readonly onDidRemoveDocuments: Event<ExtHostNotebookDocumentData[]> = this._onDidRemoveDocuments.event;
readonly onDidChangeVisibleNotebookEditors: Event<ExtHostNotebookEditor[]> = this._onDidChangeVisibleNotebookEditors.event;
readonly onDidChangeActiveNotebookEditor: Event<ExtHostNotebookEditor> = this._onDidChangeActiveNotebookEditor.event;
constructor(
private readonly _mainContext: IMainContext,
) {
if (this._mainContext) {
this._proxy = this._mainContext.getProxy(SqlMainContext.MainThreadNotebookDocumentsAndEditors);
}
}
dispose() {
this._disposables = dispose(this._disposables);
}
//#region Main Thread accessible methods
$acceptDocumentsAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): void {
const removedDocuments: ExtHostNotebookDocumentData[] = [];
const addedDocuments: ExtHostNotebookDocumentData[] = [];
const removedEditors: ExtHostNotebookEditor[] = [];
if (delta.removedDocuments) {
for (const uriComponent of delta.removedDocuments) {
const uri = URI.revive(uriComponent);
const id = uri.toString();
const data = this._documents.get(id);
this._documents.delete(id);
removedDocuments.push(data);
}
}
if (delta.addedDocuments) {
for (const data of delta.addedDocuments) {
const resource = URI.revive(data.uri);
ok(!this._documents.has(resource.toString()), `document '${resource} already exists!'`);
const documentData = new ExtHostNotebookDocumentData(
this._proxy,
resource,
data.providerId,
data.isDirty
);
this._documents.set(resource.toString(), documentData);
addedDocuments.push(documentData);
}
}
if (delta.removedEditors) {
for (const id of delta.removedEditors) {
const editor = this._editors.get(id);
this._editors.delete(id);
removedEditors.push(editor);
}
}
if (delta.addedEditors) {
for (const data of delta.addedEditors) {
const resource = URI.revive(data.documentUri);
ok(this._documents.has(resource.toString()), `document '${resource}' does not exist`);
ok(!this._editors.has(data.id), `editor '${data.id}' already exists!`);
const documentData = this._documents.get(resource.toString());
const editor = new ExtHostNotebookEditor(
this._mainContext.getProxy(SqlMainContext.MainThreadNotebookDocumentsAndEditors),
data.id,
documentData,
typeConverters.ViewColumn.to(data.editorPosition)
);
this._editors.set(data.id, editor);
}
}
if (delta.newActiveEditor !== undefined) {
ok(delta.newActiveEditor === null || this._editors.has(delta.newActiveEditor), `active editor '${delta.newActiveEditor}' does not exist`);
this._activeEditorId = delta.newActiveEditor;
}
dispose(removedDocuments);
dispose(removedEditors);
// now that the internal state is complete, fire events
if (delta.removedDocuments) {
this._onDidRemoveDocuments.fire(removedDocuments);
}
if (delta.addedDocuments) {
this._onDidAddDocuments.fire(addedDocuments);
}
if (delta.removedEditors || delta.addedEditors) {
this._onDidChangeVisibleNotebookEditors.fire(this.getAllEditors());
}
if (delta.newActiveEditor !== undefined) {
this._onDidChangeActiveNotebookEditor.fire(this.getActiveEditor());
}
}
//#endregion
//#region Extension accessible methods
showNotebookDocument(uri: vscode.Uri, showOptions: sqlops.nb.NotebookShowOptions): Thenable<sqlops.nb.NotebookEditor> {
return this.doShowNotebookDocument(uri, showOptions);
}
private async doShowNotebookDocument(uri: vscode.Uri, showOptions: sqlops.nb.NotebookShowOptions): Promise<sqlops.nb.NotebookEditor> {
let options: INotebookShowOptions = {};
if (showOptions) {
options.preserveFocus = showOptions.preserveFocus;
options.preview = showOptions.preview;
options.position = showOptions.viewColumn;
options.providerId = showOptions.providerId;
options.connectionId = showOptions.connectionId;
}
let id = await this._proxy.$tryShowNotebookDocument(uri, options);
let editor = this.getEditor(id);
if (editor) {
return editor;
} else {
throw new Error(`Failed to show notebook document ${uri.toString()}, should show in editor #${id}`);
}
}
getDocument(strUrl: string): ExtHostNotebookDocumentData {
return this._documents.get(strUrl);
}
getAllDocuments(): ExtHostNotebookDocumentData[] {
const result: ExtHostNotebookDocumentData[] = [];
this._documents.forEach(data => result.push(data));
return result;
}
getEditor(id: string): ExtHostNotebookEditor {
return this._editors.get(id);
}
getActiveEditor(): ExtHostNotebookEditor | undefined {
if (!this._activeEditorId) {
return undefined;
} else {
return this._editors.get(this._activeEditorId);
}
}
getAllEditors(): ExtHostNotebookEditor[] {
const result: ExtHostNotebookEditor[] = [];
this._editors.forEach(data => result.push(data));
return result;
}
//#endregion
}

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.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import { ok } from 'vs/base/common/assert';
import { IDisposable } from 'vs/base/common/lifecycle';
import { readonly } from 'vs/base/common/errors';
import { TPromise } from 'vs/base/common/winjs.base';
import { MainThreadNotebookDocumentsAndEditorsShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { ExtHostNotebookDocumentData } from 'sql/workbench/api/node/extHostNotebookDocumentData';
import { CellRange, ISingleNotebookEditOperation, ICellRange } from 'sql/workbench/api/common/sqlExtHostTypes';
export interface INotebookEditOperation {
range: sqlops.nb.CellRange;
cell: Partial<sqlops.nb.ICellContents>;
forceMoveMarkers: boolean;
}
export interface INotebookEditData {
documentVersionId: number;
edits: INotebookEditOperation[];
undoStopBefore: boolean;
undoStopAfter: boolean;
}
function toICellRange(range: sqlops.nb.CellRange): ICellRange {
return {
start: range.start,
end: range.end
};
}
export class NotebookEditorEdit {
private readonly _document: sqlops.nb.NotebookDocument;
private readonly _documentVersionId: number;
private _collectedEdits: INotebookEditOperation[];
private readonly _undoStopBefore: boolean;
private readonly _undoStopAfter: boolean;
constructor(document: sqlops.nb.NotebookDocument, options: { undoStopBefore: boolean; undoStopAfter: boolean; }) {
this._document = document;
// TODO add version handling
this._documentVersionId = 0;
// this._documentVersionId = document.version;
this._collectedEdits = [];
this._undoStopBefore = options ? options.undoStopBefore : true;
this._undoStopAfter = options ? options.undoStopAfter : false;
}
finalize(): INotebookEditData {
return {
documentVersionId: this._documentVersionId,
edits: this._collectedEdits,
undoStopBefore: this._undoStopBefore,
undoStopAfter: this._undoStopAfter
};
}
replace(location: number | CellRange, value: Partial<sqlops.nb.ICellContents>): void {
let range: CellRange = this.getAsRange(location);
this._pushEdit(range, value, false);
}
private getAsRange(location: number | CellRange): CellRange {
let range: CellRange = null;
if (typeof (location) === 'number') {
range = new CellRange(location, location+1);
}
else if (location instanceof CellRange) {
range = location;
}
else {
throw new Error('Unrecognized location');
}
return range;
}
insertCell(value: Partial<sqlops.nb.ICellContents>, location?: number): void {
if (location === null || location === undefined) {
// If not specified, assume adding to end of list
location = this._document.cells.length - 1;
}
this._pushEdit(new CellRange(location, location), value, true);
}
deleteCell(index: number): void {
let range: CellRange = null;
if (typeof(index) === 'number') {
// Currently only allowing single-cell deletion.
// Do this by saying the range extends over 1 cell so on the main thread
// we can delete that cell, then handle insertions
range = new CellRange(index, index+1);
} else {
throw new Error('Unrecognized index');
}
this._pushEdit(range, null, true);
}
private _pushEdit(range: sqlops.nb.CellRange, cell: Partial<sqlops.nb.ICellContents>, forceMoveMarkers: boolean): void {
let validRange = this._document.validateCellRange(range);
this._collectedEdits.push({
range: validRange,
cell: cell,
forceMoveMarkers: forceMoveMarkers
});
}
}
export class ExtHostNotebookEditor implements sqlops.nb.NotebookEditor, IDisposable {
private _disposed: boolean = false;
constructor(
private _proxy: MainThreadNotebookDocumentsAndEditorsShape,
private _id: string,
private readonly _documentData: ExtHostNotebookDocumentData,
private _viewColumn: vscode.ViewColumn
) {
}
dispose() {
ok(!this._disposed);
this._disposed = true;
}
get document(): sqlops.nb.NotebookDocument {
return this._documentData.document;
}
set document(value) {
throw readonly('document');
}
get viewColumn(): vscode.ViewColumn {
return this._viewColumn;
}
set viewColumn(value) {
throw readonly('viewColumn');
}
get id(): string {
return this._id;
}
edit(callback: (editBuilder: sqlops.nb.NotebookEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable<boolean> {
if (this._disposed) {
return TPromise.wrapError<boolean>(new Error('NotebookEditor#edit not possible on closed editors'));
}
let edit = new NotebookEditorEdit(this._documentData.document, options);
callback(edit);
return this._applyEdit(edit);
}
private _applyEdit(editBuilder: NotebookEditorEdit): TPromise<boolean> {
let editData = editBuilder.finalize();
// return when there is nothing to do
if (editData.edits.length === 0) {
return TPromise.wrap(true);
}
// check that the edits are not overlapping (i.e. illegal)
let editRanges = editData.edits.map(edit => edit.range);
// sort ascending (by end and then by start)
editRanges.sort((a, b) => {
if (a.end === b.end) {
return a.start - b.start;
}
return a.end - b.end;
});
// check that no edits are overlapping
for (let i = 0, count = editRanges.length - 1; i < count; i++) {
const rangeEnd = editRanges[i].end;
const nextRangeStart = editRanges[i + 1].start;
if (nextRangeStart < rangeEnd) {
// overlapping ranges
return TPromise.wrapError<boolean>(
new Error('Overlapping ranges are not allowed!')
);
}
}
// prepare data for serialization
let edits: ISingleNotebookEditOperation[] = editData.edits.map((edit) => {
return {
range: toICellRange(edit.range),
cell: edit.cell,
forceMoveMarkers: edit.forceMoveMarkers
};
});
return this._proxy.$tryApplyEdits(this._id, editData.documentVersionId, edits, {
undoStopBefore: editData.undoStopBefore,
undoStopAfter: editData.undoStopAfter
});
}
}

View File

@@ -19,6 +19,7 @@ import { ModelViewInput, ModelViewInputModel, ModeViewSaveHandler } from 'sql/pa
import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@extHostNamedCustomer(SqlMainContext.MainThreadModelViewDialog)
export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape {
@@ -35,7 +36,8 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape
constructor(
context: IExtHostContext,
@IInstantiationService private _instatiationService: IInstantiationService,
@IEditorService private _editorService: IEditorService
@IEditorService private _editorService: IEditorService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
this._proxy = context.getProxy(SqlExtHostContext.ExtHostModelViewDialog);
this._dialogService = new CustomDialogService(_instatiationService);
@@ -68,9 +70,9 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape
return this._proxy.$handleSave(handle);
}
public $openDialog(handle: number): Thenable<void> {
public $openDialog(handle: number, dialogName?: string): Thenable<void> {
let dialog = this.getDialog(handle);
this._dialogService.showDialog(dialog);
dialogName ? this._dialogService.showDialog(dialog, dialogName) : this._dialogService.showDialog(dialog);
return Promise.resolve();
}

View File

@@ -153,11 +153,11 @@ class ContentManagerWrapper implements sqlops.nb.ContentManager {
constructor(private handle: number, private _proxy: Proxies) {
}
getNotebookContents(notebookUri: URI): Thenable<sqlops.nb.INotebook> {
getNotebookContents(notebookUri: URI): Thenable<sqlops.nb.INotebookContents> {
return this._proxy.ext.$getNotebookContents(this.handle, notebookUri);
}
save(path: URI, notebook: sqlops.nb.INotebook): Thenable<sqlops.nb.INotebook> {
save(path: URI, notebook: sqlops.nb.INotebookContents): Thenable<sqlops.nb.INotebookContents> {
return this._proxy.ext.$save(this.handle, path, notebook);
}
}

View File

@@ -0,0 +1,412 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
import { Disposable } from 'vs/base/common/lifecycle';
import URI, { UriComponents } from 'vs/base/common/uri';
import { IExtHostContext, IUndoStopOptions } from 'vs/workbench/api/node/extHost.protocol';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { viewColumnToEditorGroup } from 'vs/workbench/api/shared/editor';
import {
SqlMainContext, MainThreadNotebookDocumentsAndEditorsShape, SqlExtHostContext, ExtHostNotebookDocumentsAndEditorsShape,
INotebookDocumentsAndEditorsDelta, INotebookEditorAddData, INotebookShowOptions, INotebookModelAddedData
} from 'sql/workbench/api/node/sqlExtHost.protocol';
import { NotebookInputModel, NotebookInput } from 'sql/parts/notebook/notebookInput';
import { INotebookService, INotebookEditor } from 'sql/services/notebook/notebookService';
import { TPromise } from 'vs/base/common/winjs.base';
import { getProviderForFileName } from 'sql/parts/notebook/notebookUtils';
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
import { disposed } from 'vs/base/common/errors';
class MainThreadNotebookEditor extends Disposable {
constructor(public readonly editor: INotebookEditor) {
super();
}
public get uri(): URI {
return this.editor.notebookParams.notebookUri;
}
public get id(): string {
return this.editor.id;
}
public get isDirty(): boolean {
return this.editor.isDirty();
}
public get providerId(): string {
return this.editor.notebookParams.providerId;
}
public save(): Thenable<boolean> {
return this.editor.save();
}
public matches(input: NotebookInput): boolean {
if (!input) {
return false;
}
return input === this.editor.notebookParams.input;
}
public applyEdits(versionIdCheck: number, edits: ISingleNotebookEditOperation[], opts: IUndoStopOptions): boolean {
// TODO Handle version tracking
// if (this._model.getVersionId() !== versionIdCheck) {
// // throw new Error('Model has changed in the meantime!');
// // model changed in the meantime
// return false;
// }
if (!this.editor) {
// console.warn('applyEdits on invisible editor');
return false;
}
// TODO handle undo tracking
// if (opts.undoStopBefore) {
// this._codeEditor.pushUndoStop();
// }
this.editor.executeEdits(edits);
// if (opts.undoStopAfter) {
// this._codeEditor.pushUndoStop();
// }
return true;
}
}
function wait(timeMs: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, timeMs));
}
namespace mapset {
export function setValues<T>(set: Set<T>): T[] {
// return Array.from(set);
let ret: T[] = [];
set.forEach(v => ret.push(v));
return ret;
}
export function mapValues<T>(map: Map<any, T>): T[] {
// return Array.from(map.values());
let ret: T[] = [];
map.forEach(v => ret.push(v));
return ret;
}
}
namespace delta {
export function ofSets<T>(before: Set<T>, after: Set<T>): { removed: T[], added: T[] } {
const removed: T[] = [];
const added: T[] = [];
before.forEach(element => {
if (!after.has(element)) {
removed.push(element);
}
});
after.forEach(element => {
if (!before.has(element)) {
added.push(element);
}
});
return { removed, added };
}
export function ofMaps<K, V>(before: Map<K, V>, after: Map<K, V>): { removed: V[], added: V[] } {
const removed: V[] = [];
const added: V[] = [];
before.forEach((value, index) => {
if (!after.has(index)) {
removed.push(value);
}
});
after.forEach((value, index) => {
if (!before.has(index)) {
added.push(value);
}
});
return { removed, added };
}
}
class NotebookEditorStateDelta {
readonly isEmpty: boolean;
constructor(
readonly removedEditors: INotebookEditor[],
readonly addedEditors: INotebookEditor[],
readonly oldActiveEditor: string,
readonly newActiveEditor: string,
) {
this.isEmpty =
this.removedEditors.length === 0
&& this.addedEditors.length === 0
&& oldActiveEditor === newActiveEditor;
}
toString(): string {
let ret = 'NotebookEditorStateDelta\n';
ret += `\tRemoved Editors: [${this.removedEditors.map(e => e.id).join(', ')}]\n`;
ret += `\tAdded Editors: [${this.addedEditors.map(e => e.id).join(', ')}]\n`;
ret += `\tNew Active Editor: ${this.newActiveEditor}\n`;
return ret;
}
}
class NotebookEditorState {
static compute(before: NotebookEditorState, after: NotebookEditorState): NotebookEditorStateDelta {
if (!before) {
return new NotebookEditorStateDelta(
[], mapset.mapValues(after.textEditors),
undefined, after.activeEditor
);
}
const editorDelta = delta.ofMaps(before.textEditors, after.textEditors);
const oldActiveEditor = before.activeEditor !== after.activeEditor ? before.activeEditor : undefined;
const newActiveEditor = before.activeEditor !== after.activeEditor ? after.activeEditor : undefined;
return new NotebookEditorStateDelta(
editorDelta.removed, editorDelta.added,
oldActiveEditor, newActiveEditor
);
}
constructor(
readonly textEditors: Map<string, INotebookEditor>,
readonly activeEditor: string) { }
}
class MainThreadNotebookDocumentAndEditorStateComputer extends Disposable {
private _currentState: NotebookEditorState;
constructor(
private readonly _onDidChangeState: (delta: NotebookEditorStateDelta) => void,
@IEditorService private readonly _editorService: IEditorService,
@INotebookService private readonly _notebookService: INotebookService
) {
super();
this._register(this._editorService.onDidActiveEditorChange(this._updateState, this));
this._register(this._editorService.onDidVisibleEditorsChange(this._updateState, this));
this._register(this._notebookService.onNotebookEditorAdd(this._onDidAddEditor, this));
this._register(this._notebookService.onNotebookEditorRemove(this._onDidRemoveEditor, this));
this._updateState();
}
private _onDidAddEditor(e: INotebookEditor): void {
// TODO hook to cell change and other events
this._updateState();
}
private _onDidRemoveEditor(e: INotebookEditor): void {
// TODO remove event listeners
this._updateState();
}
private _updateState(): void {
// editor
const editors = new Map<string, INotebookEditor>();
let activeEditor: string = undefined;
for (const editor of this._notebookService.listNotebookEditors()) {
editors.set(editor.id, editor);
if (editor.isActive()) {
activeEditor = editor.id;
}
}
// compute new state and compare against old
const newState = new NotebookEditorState(editors, activeEditor);
const delta = NotebookEditorState.compute(this._currentState, newState);
if (!delta.isEmpty) {
this._currentState = newState;
this._onDidChangeState(delta);
}
}
}
@extHostNamedCustomer(SqlMainContext.MainThreadNotebookDocumentsAndEditors)
export class MainThreadNotebookDocumentsAndEditors extends Disposable implements MainThreadNotebookDocumentsAndEditorsShape {
private _proxy: ExtHostNotebookDocumentsAndEditorsShape;
private _notebookEditors = new Map<string, MainThreadNotebookEditor>();
constructor(
extHostContext: IExtHostContext,
@IInstantiationService private _instantiationService: IInstantiationService,
@IEditorService private _editorService: IEditorService,
@IEditorGroupsService private _editorGroupService: IEditorGroupsService
) {
super();
if (extHostContext) {
this._proxy = extHostContext.getProxy(SqlExtHostContext.ExtHostNotebookDocumentsAndEditors);
}
// Create a state computer that actually tracks all required changes. This is hooked to onDelta which notifies extension host
this._register(this._instantiationService.createInstance(MainThreadNotebookDocumentAndEditorStateComputer, delta => this._onDelta(delta)));
}
//#region extension host callable APIs
$trySaveDocument(uri: UriComponents): Thenable<boolean> {
let uriString = URI.revive(uri).toString();
let editor = this._notebookEditors.get(uriString);
if (editor) {
return editor.save();
} else {
return Promise.resolve(false);
}
}
$tryShowNotebookDocument(resource: UriComponents, options: INotebookShowOptions): TPromise<string> {
return TPromise.wrap(this.doOpenEditor(resource, options));
}
$tryApplyEdits(id: string, modelVersionId: number, edits: ISingleNotebookEditOperation[], opts: IUndoStopOptions): TPromise<boolean> {
let editor = this.getEditor(id);
if (!editor) {
return TPromise.wrapError<boolean>(disposed(`TextEditor(${id})`));
}
return TPromise.as(editor.applyEdits(modelVersionId, edits, opts));
}
//#endregion
private async doOpenEditor(resource: UriComponents, options: INotebookShowOptions): Promise<string> {
const uri = URI.revive(resource);
const editorOptions: ITextEditorOptions = {
preserveFocus: options.preserveFocus,
pinned: !options.preview
};
let model = new NotebookInputModel(uri, undefined, false, undefined);
let providerId = options.providerId;
if(!providerId)
{
// Ensure there is always a sensible provider ID for this file type
providerId = getProviderForFileName(uri.fsPath);
}
model.providerId = providerId;
let input = this._instantiationService.createInstance(NotebookInput, undefined, model);
let editor = await this._editorService.openEditor(input, editorOptions, viewColumnToEditorGroup(this._editorGroupService, options.position));
if (!editor) {
return undefined;
}
return this.waitOnEditor(input);
}
private async waitOnEditor(input: NotebookInput): Promise<string> {
let id: string = undefined;
let attemptsLeft = 10;
let timeoutMs = 20;
while (!id && attemptsLeft > 0) {
id = this.findNotebookEditorIdFor(input);
if (!id) {
await wait(timeoutMs);
}
}
return id;
}
findNotebookEditorIdFor(input: NotebookInput): string {
let foundId: string = undefined;
this._notebookEditors.forEach(e => {
if (e.matches(input)) {
foundId = e.id;
}
});
return foundId;
}
getEditor(id: string): MainThreadNotebookEditor {
return this._notebookEditors.get(id);
}
private _onDelta(delta: NotebookEditorStateDelta): void {
let removedEditors: string[] = [];
let removedDocuments: URI[] = [];
let addedEditors: MainThreadNotebookEditor[] = [];
// added editors
for (const editor of delta.addedEditors) {
const mainThreadEditor = new MainThreadNotebookEditor(editor);
this._notebookEditors.set(editor.id, mainThreadEditor);
addedEditors.push(mainThreadEditor);
}
// removed editors
for (const { id } of delta.removedEditors) {
const mainThreadEditor = this._notebookEditors.get(id);
if (mainThreadEditor) {
removedDocuments.push(mainThreadEditor.uri);
mainThreadEditor.dispose();
this._notebookEditors.delete(id);
removedEditors.push(id);
}
}
let extHostDelta: INotebookDocumentsAndEditorsDelta = Object.create(null);
let empty = true;
if (delta.newActiveEditor !== undefined) {
empty = false;
extHostDelta.newActiveEditor = delta.newActiveEditor;
}
if (removedDocuments.length > 0) {
empty = false;
extHostDelta.removedDocuments = removedDocuments;
}
if (removedEditors.length > 0) {
empty = false;
extHostDelta.removedEditors = removedEditors;
}
if (delta.addedEditors.length > 0) {
empty = false;
extHostDelta.addedDocuments = [];
extHostDelta.addedEditors = [];
for (let editor of addedEditors) {
extHostDelta.addedEditors.push(this._toNotebookEditorAddData(editor));
// For now, add 1 document for each editor. In the future these may be trackable independently
extHostDelta.addedDocuments.push(this._toNotebookModelAddData(editor));
}
}
if (!empty) {
this._proxy.$acceptDocumentsAndEditorsDelta(extHostDelta);
}
}
private _toNotebookEditorAddData(editor: MainThreadNotebookEditor): INotebookEditorAddData {
let addData: INotebookEditorAddData = {
documentUri: editor.uri,
editorPosition: undefined,
id: editor.editor.id
};
return addData;
}
private _toNotebookModelAddData(editor: MainThreadNotebookEditor): INotebookModelAddedData {
let addData: INotebookModelAddedData = {
uri: editor.uri,
isDirty: editor.isDirty,
providerId: editor.providerId
};
return addData;
}
}

View File

@@ -38,6 +38,7 @@ import { ExtHostModelViewTreeViews } from 'sql/workbench/api/node/extHostModelVi
import { ExtHostQueryEditor } from 'sql/workbench/api/node/extHostQueryEditor';
import { ExtHostBackgroundTaskManagement } from './extHostBackgroundTaskManagement';
import { ExtHostNotebook } from 'sql/workbench/api/node/extHostNotebook';
import { ExtHostNotebookDocumentsAndEditors } from 'sql/workbench/api/node/extHostNotebookDocumentsAndEditors';
export interface ISqlExtensionApiFactory {
vsCodeFactory(extension: IExtensionDescription): typeof vscode;
@@ -75,6 +76,7 @@ export function createApiFactory(
const extHostModelViewDialog = rpcProtocol.set(SqlExtHostContext.ExtHostModelViewDialog, new ExtHostModelViewDialog(rpcProtocol, extHostModelView, extHostBackgroundTaskManagement));
const extHostQueryEditor = rpcProtocol.set(SqlExtHostContext.ExtHostQueryEditor, new ExtHostQueryEditor(rpcProtocol));
const extHostNotebook = rpcProtocol.set(SqlExtHostContext.ExtHostNotebook, new ExtHostNotebook(rpcProtocol));
const extHostNotebookDocumentsAndEditors = rpcProtocol.set(SqlExtHostContext.ExtHostNotebookDocumentsAndEditors, new ExtHostNotebookDocumentsAndEditors(rpcProtocol));
return {
@@ -346,8 +348,8 @@ export function createApiFactory(
};
const modelViewDialog: typeof sqlops.window.modelviewdialog = {
createDialog(title: string): sqlops.window.modelviewdialog.Dialog {
return extHostModelViewDialog.createDialog(title, extension.extensionLocation);
createDialog(title: string, dialogName?: string): sqlops.window.modelviewdialog.Dialog {
return extHostModelViewDialog.createDialog(title, dialogName, extension.extensionLocation);
},
createTab(title: string): sqlops.window.modelviewdialog.DialogTab {
return extHostModelViewDialog.createTab(title, extension.extensionLocation);
@@ -420,9 +422,28 @@ export function createApiFactory(
};
const nb = {
get notebookDocuments() {
return extHostNotebookDocumentsAndEditors.getAllDocuments().map(doc => doc.document);
},
get activeNotebookEditor() {
return extHostNotebookDocumentsAndEditors.getActiveEditor();
},
get visibleNotebookEditors() {
return extHostNotebookDocumentsAndEditors.getAllEditors();
},
get onDidOpenNotebookDocument() {
return extHostNotebook.onDidOpenNotebookDocument;
},
get onDidChangeNotebookCell() {
return extHostNotebook.onDidChangeNotebookCell;
},
showNotebookDocument(uri: vscode.Uri, showOptions: sqlops.nb.NotebookShowOptions) {
return extHostNotebookDocumentsAndEditors.showNotebookDocument(uri, showOptions);
},
registerNotebookProvider(provider: sqlops.nb.NotebookProvider): vscode.Disposable {
return extHostNotebook.registerNotebookProvider(provider);
}
},
CellRange: sqlExtHostTypes.CellRange
};
return {

View File

@@ -24,6 +24,7 @@ import 'sql/workbench/api/node/mainThreadQueryEditor';
import 'sql/workbench/api/node/mainThreadModelView';
import 'sql/workbench/api/node/mainThreadModelViewDialog';
import 'sql/workbench/api/node/mainThreadNotebook';
import 'sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors';
import 'sql/workbench/api/node/mainThreadAccountManagement';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';

View File

@@ -21,8 +21,10 @@ import { ITreeComponentItem } from 'sql/workbench/common/views';
import { ITaskHandlerDescription } from 'sql/platform/tasks/common/tasks';
import {
IItemConfig, ModelComponentTypes, IComponentShape, IModelViewDialogDetails, IModelViewTabDetails, IModelViewButtonDetails,
IModelViewWizardDetails, IModelViewWizardPageDetails, INotebookManagerDetails, INotebookSessionDetails, INotebookKernelDetails, INotebookFutureDetails, FutureMessageType, INotebookFutureDone
IModelViewWizardDetails, IModelViewWizardPageDetails, INotebookManagerDetails, INotebookSessionDetails, INotebookKernelDetails, INotebookFutureDetails, FutureMessageType, INotebookFutureDone, ISingleNotebookEditOperation
} from 'sql/workbench/api/common/sqlExtHostTypes';
import { EditorViewColumn } from 'vs/workbench/api/shared/editor';
import { IUndoStopOptions } from 'vs/workbench/api/node/extHost.protocol';
export abstract class ExtHostAccountManagementShape {
$autoOAuthCancelled(handle: number): Thenable<void> { throw ni(); }
@@ -572,7 +574,9 @@ export const SqlMainContext = {
MainThreadDashboard: createMainId<MainThreadDashboardShape>('MainThreadDashboard'),
MainThreadModelViewDialog: createMainId<MainThreadModelViewDialogShape>('MainThreadModelViewDialog'),
MainThreadQueryEditor: createMainId<MainThreadQueryEditorShape>('MainThreadQueryEditor'),
MainThreadNotebook: createMainId<MainThreadNotebookShape>('MainThreadNotebook')
MainThreadNotebook: createMainId<MainThreadNotebookShape>('MainThreadNotebook'),
MainThreadNotebookDocumentsAndEditors: createMainId<MainThreadNotebookDocumentsAndEditorsShape>('MainThreadNotebookDocumentsAndEditors')
};
export const SqlExtHostContext = {
@@ -592,7 +596,8 @@ export const SqlExtHostContext = {
ExtHostDashboard: createExtId<ExtHostDashboardShape>('ExtHostDashboard'),
ExtHostModelViewDialog: createExtId<ExtHostModelViewDialogShape>('ExtHostModelViewDialog'),
ExtHostQueryEditor: createExtId<ExtHostQueryEditorShape>('ExtHostQueryEditor'),
ExtHostNotebook: createExtId<ExtHostNotebookShape>('ExtHostNotebook')
ExtHostNotebook: createExtId<ExtHostNotebookShape>('ExtHostNotebook'),
ExtHostNotebookDocumentsAndEditors: createExtId<ExtHostNotebookDocumentsAndEditorsShape>('ExtHostNotebookDocumentsAndEditors')
};
export interface MainThreadDashboardShape extends IDisposable {
@@ -712,7 +717,7 @@ export interface ExtHostModelViewDialogShape {
export interface MainThreadModelViewDialogShape extends IDisposable {
$openEditor(handle: number, modelViewId: string, title: string, options?: sqlops.ModelViewEditorOptions, position?: vscode.ViewColumn): Thenable<void>;
$openDialog(handle: number): Thenable<void>;
$openDialog(handle: number, dialogName?: string): Thenable<void>;
$closeDialog(handle: number): Thenable<void>;
$setDialogDetails(handle: number, details: IModelViewDialogDetails): Thenable<void>;
$setTabDetails(handle: number, details: IModelViewTabDetails): Thenable<void>;
@@ -750,8 +755,8 @@ export interface ExtHostNotebookShape {
$doStopServer(managerHandle: number): Thenable<void>;
// Content Manager APIs
$getNotebookContents(managerHandle: number, notebookUri: UriComponents): Thenable<sqlops.nb.INotebook>;
$save(managerHandle: number, notebookUri: UriComponents, notebook: sqlops.nb.INotebook): Thenable<sqlops.nb.INotebook>;
$getNotebookContents(managerHandle: number, notebookUri: UriComponents): Thenable<sqlops.nb.INotebookContents>;
$save(managerHandle: number, notebookUri: UriComponents, notebook: sqlops.nb.INotebookContents): Thenable<sqlops.nb.INotebookContents>;
// Session Manager APIs
$refreshSpecs(managerHandle: number): Thenable<sqlops.nb.IAllKernels>;
@@ -780,3 +785,40 @@ export interface MainThreadNotebookShape extends IDisposable {
$onFutureDone(futureId: number, done: INotebookFutureDone): void;
}
export interface INotebookDocumentsAndEditorsDelta {
removedDocuments?: UriComponents[];
addedDocuments?: INotebookModelAddedData[];
removedEditors?: string[];
addedEditors?: INotebookEditorAddData[];
newActiveEditor?: string;
}
export interface INotebookModelAddedData {
uri: UriComponents;
providerId: string;
isDirty: boolean;
}
export interface INotebookEditorAddData {
id: string;
documentUri: UriComponents;
editorPosition: EditorViewColumn;
}
export interface INotebookShowOptions {
position?: EditorViewColumn;
preserveFocus?: boolean;
preview?: boolean;
providerId?: string;
connectionId?: string;
}
export interface ExtHostNotebookDocumentsAndEditorsShape {
$acceptDocumentsAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): void;
}
export interface MainThreadNotebookDocumentsAndEditorsShape extends IDisposable {
$trySaveDocument(uri: UriComponents): Thenable<boolean>;
$tryShowNotebookDocument(resource: UriComponents, options: INotebookShowOptions): TPromise<string>;
$tryApplyEdits(id: string, modelVersionId: number, edits: ISingleNotebookEditOperation[], opts: IUndoStopOptions): TPromise<boolean>;
}

View File

@@ -881,4 +881,45 @@ suite('SQL ConnectionManagementService tests', () => {
assert.equal(profileWithCredentials.userName, username);
assert.equal(profileWithCredentials.options['azureAccountToken'], testToken);
});
test('addSavedPassword fills in Azure access token for selected tenant', async () => {
// Set up a connection profile that uses Azure
let azureConnectionProfile = ConnectionProfile.fromIConnectionProfile(capabilitiesService, connectionProfile);
azureConnectionProfile.authenticationType = 'AzureMFA';
let username = 'testuser@microsoft.com';
azureConnectionProfile.userName = username;
let servername = 'test-database.database.windows.net';
azureConnectionProfile.serverName = servername;
let azureTenantId = 'testTenant';
azureConnectionProfile.azureTenantId = azureTenantId;
// Set up the account management service to return a token for the given user
accountManagementService.setup(x => x.getAccountsForProvider(TypeMoq.It.isAny())).returns(providerId => Promise.resolve<sqlops.Account[]>([
{
key: {
accountId: username,
providerId: providerId
},
displayInfo: undefined,
isStale: false,
properties: undefined
}
]));
let testToken = 'testToken';
let returnedTokens = {};
returnedTokens['azurePublicCloud'] = { token: 'badToken' };
returnedTokens[azureTenantId] = { token: testToken };
accountManagementService.setup(x => x.getSecurityToken(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(returnedTokens));
connectionStore.setup(x => x.addSavedPassword(TypeMoq.It.is(profile => profile.authenticationType === 'AzureMFA'))).returns(profile => Promise.resolve({
profile: profile,
savedCred: false
}));
// If I call addSavedPassword
let profileWithCredentials = await connectionManagementService.addSavedPassword(azureConnectionProfile);
// Then the returned profile has the account token set corresponding to the requested tenant
assert.equal(profileWithCredentials.userName, username);
assert.equal(profileWithCredentials.options['azureAccountToken'], testToken);
});
});

View File

@@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { nb, IConnectionProfile } from 'sqlops';
import { Event, Emitter } from 'vs/base/common/event';
import { INotebookModel, ICellModel, IClientSession, IDefaultConnection } from 'sql/parts/notebook/models/modelInterfaces';
import { NotebookChangeType, CellType } from 'sql/parts/notebook/models/contracts';
import { INotebookManager } from 'sql/services/notebook/notebookService';
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
export class NotebookModelStub implements INotebookModel {
constructor(private _languageInfo?: nb.ILanguageInfo) {
}
public trustedMode: boolean;
public get languageInfo(): nb.ILanguageInfo {
return this._languageInfo;
}
onCellChange(cell: ICellModel, change: NotebookChangeType): void {
// Default: do nothing
}
get cells(): ReadonlyArray<ICellModel> {
throw new Error('method not implemented.');
}
get clientSession(): IClientSession {
throw new Error('method not implemented.');
}
get notebookManager(): INotebookManager {
throw new Error('method not implemented.');
}
get kernelChanged(): Event<nb.IKernelChangedArgs> {
throw new Error('method not implemented.');
}
get kernelsChanged(): Event<nb.IKernelSpec> {
throw new Error('method not implemented.');
} get defaultKernel(): nb.IKernelSpec {
throw new Error('method not implemented.');
}
get contextsChanged(): Event<void> {
throw new Error('method not implemented.');
}
get specs(): nb.IAllKernels {
throw new Error('method not implemented.');
}
get contexts(): IDefaultConnection {
throw new Error('method not implemented.');
}
changeKernel(displayName: string): void {
throw new Error('Method not implemented.');
}
changeContext(host: string, connection?: IConnectionProfile): void {
throw new Error('Method not implemented.');
}
findCellIndex(cellModel: ICellModel): number {
throw new Error('Method not implemented.');
}
addCell(cellType: CellType, index?: number): void {
throw new Error('Method not implemented.');
}
deleteCell(cellModel: ICellModel): void {
throw new Error('Method not implemented.');
}
saveModel(): Promise<boolean> {
throw new Error('Method not implemented.');
}
pushEditOperations(edits: ISingleNotebookEditOperation[]): void {
throw new Error('Method not implemented.');
}
}
export class NotebookManagerStub implements INotebookManager {
providerId: string;
contentManager: nb.ContentManager;
sessionManager: nb.SessionManager;
serverManager: nb.ServerManager;
}
export class ServerManagerStub implements nb.ServerManager {
public onServerStartedEmitter = new Emitter<void>();
onServerStarted: Event<void> = this.onServerStartedEmitter.event;
isStarted: boolean = false;
calledStart: boolean = false;
calledEnd: boolean = false;
public result: Promise<void> = undefined;
startServer(): Promise<void> {
this.calledStart = true;
return this.result;
}
stopServer(): Promise<void> {
this.calledEnd = true;
return this.result;
}
}

View File

@@ -0,0 +1,262 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import { nb } from 'sqlops';
import * as objects from 'vs/base/common/objects';
import { CellTypes } from 'sql/parts/notebook/models/contracts';
import { ModelFactory } from 'sql/parts/notebook/models/modelFactory';
import { NotebookModelStub } from '../common';
import { EmptyFuture } from 'sql/services/notebook/sessionManager';
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
describe('Cell Model', function (): void {
let factory = new ModelFactory();
it('Should set default values if none defined', async function (): Promise<void> {
let cell = factory.createCell(undefined, undefined);
should(cell.cellType).equal(CellTypes.Code);
should(cell.source).equal('');
});
it('Should update values', async function (): Promise<void> {
let cell = factory.createCell(undefined, undefined);
cell.language = 'sql';
should(cell.language).equal('sql');
cell.source = 'abcd';
should(cell.source).equal('abcd');
});
it('Should match ICell values if defined', async function (): Promise<void> {
let output: nb.IStreamResult = {
output_type: 'stream',
text: 'Some output',
name: 'stdout'
};
let cellData: nb.ICellContents = {
cell_type: CellTypes.Markdown,
source: 'some *markdown*',
outputs: [output],
metadata: { language: 'python'},
execution_count: 1
};
let cell = factory.createCell(cellData, undefined);
should(cell.cellType).equal(cellData.cell_type);
should(cell.source).equal(cellData.source);
should(cell.outputs).have.length(1);
should(cell.outputs[0].output_type).equal('stream');
should((<nb.IStreamResult>cell.outputs[0]).text).equal('Some output');
});
it('Should set cell language to python if defined as python in languageInfo', async function (): Promise<void> {
let cellData: nb.ICellContents = {
cell_type: CellTypes.Code,
source: 'print(\'1\')',
metadata: { language: 'python'},
execution_count: 1
};
let notebookModel = new NotebookModelStub({
name: 'python',
version: '',
mimetype: ''
});
let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false });
should(cell.language).equal('python');
});
it('Should set cell language to python if defined as pyspark in languageInfo', async function (): Promise<void> {
let cellData: nb.ICellContents = {
cell_type: CellTypes.Code,
source: 'print(\'1\')',
metadata: { language: 'python'},
execution_count: 1
};
let notebookModel = new NotebookModelStub({
name: 'pyspark',
version: '',
mimetype: ''
});
let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false });
should(cell.language).equal('python');
});
it('Should set cell language to scala if defined as scala in languageInfo', async function (): Promise<void> {
let cellData: nb.ICellContents = {
cell_type: CellTypes.Code,
source: 'print(\'1\')',
metadata: { language: 'python'},
execution_count: 1
};
let notebookModel = new NotebookModelStub({
name: 'scala',
version: '',
mimetype: ''
});
let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false });
should(cell.language).equal('scala');
});
it('Should set cell language to python if no language defined', async function (): Promise<void> {
let cellData: nb.ICellContents = {
cell_type: CellTypes.Code,
source: 'print(\'1\')',
metadata: { language: 'python'},
execution_count: 1
};
let notebookModel = new NotebookModelStub({
name: '',
version: '',
mimetype: ''
});
let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false });
should(cell.language).equal('python');
});
it('Should match cell language to language specified if unknown language defined in languageInfo', async function (): Promise<void> {
let cellData: nb.ICellContents = {
cell_type: CellTypes.Code,
source: 'std::cout << "hello world";',
metadata: { language: 'python'},
execution_count: 1
};
let notebookModel = new NotebookModelStub({
name: 'cplusplus',
version: '',
mimetype: ''
});
let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false });
should(cell.language).equal('cplusplus');
});
it('Should match cell language to mimetype name is not supplied in languageInfo', async function (): Promise<void> {
let cellData: nb.ICellContents = {
cell_type: CellTypes.Code,
source: 'print(\'1\')',
metadata: { language: 'python'},
execution_count: 1
};
let notebookModel = new NotebookModelStub({
name: '',
version: '',
mimetype: 'x-scala'
});
let cell = factory.createCell(cellData, { notebook: notebookModel, isTrusted: false });
should(cell.language).equal('scala');
});
describe('Model Future handling', function(): void {
let future: TypeMoq.Mock<EmptyFuture>;
let cell: ICellModel;
beforeEach(() => {
future = TypeMoq.Mock.ofType(EmptyFuture);
cell = factory.createCell({
cell_type: CellTypes.Code,
source: 'print "Hello"',
metadata: { language: 'python'},
execution_count: 1
}, {
notebook: new NotebookModelStub({
name: '',
version: '',
mimetype: 'x-scala'
}),
isTrusted: false
});
});
it('should send and handle incoming messages', async () => {
// Given a future
let onReply: nb.MessageHandler<nb.IShellMessage>;
let onIopub: nb.MessageHandler<nb.IIOPubMessage>;
future.setup(f => f.setReplyHandler(TypeMoq.It.isAny())).callback((handler) => onReply = handler);
future.setup(f => f.setIOPubHandler(TypeMoq.It.isAny())).callback((handler) => onIopub = handler);
let outputs: ReadonlyArray<nb.ICellOutput> = undefined;
cell.onOutputsChanged((o => outputs = o));
// When I set it on the cell
cell.setFuture(future.object);
// Then I expect outputs to have been cleared
should(outputs).have.length(0);
should(onReply).not.be.undefined();
// ... And when I send an IoPub message
let message: nb.IIOPubMessage = {
channel: 'iopub',
type: 'iopub',
parent_header: undefined,
metadata: undefined,
header: <nb.IHeader> {
msg_type: 'stream'
},
content: {
text: 'Printed hello world'
}
};
onIopub.handle(message);
// Then I expect an output to be added
should(outputs).have.length(1);
should(outputs[0].output_type).equal('stream');
message = objects.deepClone(message);
message.header.msg_type = 'display_data';
onIopub.handle(message);
should(outputs[1].output_type).equal('display_data');
// ... TODO: And when I sent a reply I expect it to be processed.
});
it('should delete transient tag while handling incoming messages', async () => {
// Given a future
let onIopub: nb.MessageHandler<nb.IIOPubMessage>;
future.setup(f => f.setIOPubHandler(TypeMoq.It.isAny())).callback((handler) => onIopub = handler);
let outputs: ReadonlyArray<nb.ICellOutput> = undefined;
cell.onOutputsChanged((o => outputs = o));
//Set the future
cell.setFuture(future.object);
// ... And when I send an IoPub message
let message: nb.IIOPubMessage = {
channel: 'iopub',
type: 'iopub',
parent_header: undefined,
metadata: undefined,
header: <nb.IHeader> {
msg_type: 'display_data'
},
content: {
text: 'Printed hello world',
transient: 'transient data'
}
};
onIopub.handle(message);
//Output array's length should be 1
//'transient' tag should no longer exist in the output
should(outputs).have.length(1);
should(outputs[0]['transient']).be.undefined();
});
it('should dispose old future', async () => {
let oldFuture = TypeMoq.Mock.ofType(EmptyFuture);
cell.setFuture(oldFuture.object);
cell.setFuture(future.object);
oldFuture.verify(f => f.dispose(), TypeMoq.Times.once());
});
});
});

View File

@@ -0,0 +1,202 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import { nb } from 'sqlops';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import URI from 'vs/base/common/uri';
import { ClientSession } from 'sql/parts/notebook/models/clientSession';
import { SessionManager, EmptySession } from 'sql/services/notebook/sessionManager';
import { NotebookManagerStub, ServerManagerStub } from 'sqltest/parts/notebook/common';
describe('Client Session', function(): void {
let path = URI.file('my/notebook.ipynb');
let notebookManager: NotebookManagerStub;
let serverManager: ServerManagerStub;
let mockSessionManager: TypeMoq.Mock<nb.SessionManager>;
let notificationService: TypeMoq.Mock<INotificationService>;
let session: ClientSession;
let remoteSession: ClientSession;
beforeEach(() => {
serverManager = new ServerManagerStub();
mockSessionManager = TypeMoq.Mock.ofType(SessionManager);
notebookManager = new NotebookManagerStub();
notebookManager.serverManager = serverManager;
notebookManager.sessionManager = mockSessionManager.object;
notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose);
session = new ClientSession({
notebookManager: notebookManager,
notebookUri: path,
notificationService: notificationService.object
});
let serverlessNotebookManager = new NotebookManagerStub();
serverlessNotebookManager.sessionManager = mockSessionManager.object;
remoteSession = new ClientSession({
notebookManager: serverlessNotebookManager,
notebookUri: path,
notificationService: notificationService.object
});
});
it('Should set path, isReady and ready on construction', function(): void {
should(session.notebookUri).equal(path);
should(session.ready).not.be.undefined();
should(session.isReady).be.false();
should(session.status).equal('starting');
should(session.isInErrorState).be.false();
should(session.errorMessage).be.undefined();
});
it('Should call on serverManager startup if set', async function(): Promise<void> {
// Given I have a serverManager that starts successfully
serverManager.result = Promise.resolve();
should(session.isReady).be.false();
// When I kick off initialization
await session.initialize();
// Then I expect ready to be completed too
await session.ready;
should(serverManager.calledStart).be.true();
should(session.isReady).be.true();
});
it('Should go to error state if serverManager startup fails', async function(): Promise<void> {
// Given I have a serverManager that fails to start
serverManager.result = Promise.reject('error');
should(session.isInErrorState).be.false();
// When I initialize
await session.initialize();
// Then I expect ready to complete, but isInErrorState to be true
await session.ready;
should(session.isReady).be.true();
should(serverManager.calledStart).be.true();
should(session.isInErrorState).be.true();
should(session.errorMessage).equal('error');
});
it('Should be ready when session manager is ready', async function(): Promise<void> {
serverManager.result = new Promise((resolve) => {
serverManager.isStarted = true;
resolve();
});
mockSessionManager.setup(s => s.ready).returns(() => Promise.resolve());
// When I call initialize
await session.initialize();
// Then
should(session.isReady).be.true();
should(session.isInErrorState).be.false();
await session.ready;
});
it('Should be in error state if server fails to start', async function(): Promise<void> {
serverManager.result = new Promise((resolve) => {
serverManager.isStarted = false;
resolve();
});
mockSessionManager.setup(s => s.ready).returns(() => Promise.resolve());
// When I call initialize
await session.initialize();
// Then
await session.ready;
should(session.isReady).be.true();
should(session.isInErrorState).be.true();
});
it('Should go to error state if sessionManager fails', async function(): Promise<void> {
serverManager.isStarted = true;
mockSessionManager.setup(s => s.isReady).returns(() => false);
mockSessionManager.setup(s => s.ready).returns(() => Promise.reject('error'));
// When I call initialize
await session.initialize();
// Then
should(session.isReady).be.true();
should(session.isInErrorState).be.true();
should(session.errorMessage).equal('error');
});
it('Should start session automatically if kernel preference requests it', async function(): Promise<void> {
serverManager.isStarted = true;
mockSessionManager.setup(s => s.ready).returns(() => Promise.resolve());
let sessionMock = TypeMoq.Mock.ofType(EmptySession);
let startOptions: nb.ISessionOptions = undefined;
mockSessionManager.setup(s => s.startNew(TypeMoq.It.isAny())).returns((options) => {
startOptions = options;
return Promise.resolve(sessionMock.object);
});
// When I call initialize after defining kernel preferences
session.kernelPreference = {
shouldStart: true,
name: 'python'
};
await session.initialize();
// Then
should(session.isReady).be.true();
should(session.isInErrorState).be.false();
should(startOptions.kernelName).equal('python');
should(startOptions.path).equal(path.fsPath);
});
it('Should shutdown session even if no serverManager is set', async function(): Promise<void> {
// Given a session against a remote server
let expectedId = 'abc';
mockSessionManager.setup(s => s.isReady).returns(() => true);
mockSessionManager.setup(s => s.shutdown(TypeMoq.It.isAny())).returns(() => Promise.resolve());
let sessionMock = TypeMoq.Mock.ofType(EmptySession);
sessionMock.setup(s => s.id).returns(() => expectedId);
mockSessionManager.setup(s => s.startNew(TypeMoq.It.isAny())).returns(() => Promise.resolve(sessionMock.object));
remoteSession.kernelPreference = {
shouldStart: true,
name: 'python'
};
await remoteSession.initialize();
// When I call shutdown
await remoteSession.shutdown();
// Then
mockSessionManager.verify(s => s.shutdown(TypeMoq.It.isValue(expectedId)), TypeMoq.Times.once());
});
it('Should stop server if server is set', async function(): Promise<void> {
// Given a kernel has been started
serverManager.isStarted = true;
serverManager.result = Promise.resolve();
mockSessionManager.setup(s => s.isReady).returns(() => true);
mockSessionManager.setup(s => s.shutdown(TypeMoq.It.isAny())).returns(() => Promise.resolve());
await session.initialize();
// When I call shutdown
await session.shutdown();
// Then
should(serverManager.calledEnd).be.true();
});
});

View File

@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import * as path from 'path';
import { nb } from 'sqlops';
import URI from 'vs/base/common/uri';
import * as tempWrite from 'temp-write';
import { LocalContentManager } from 'sql/services/notebook/localContentManager';
import * as testUtils from '../../../utils/testUtils';
import { CellTypes } from 'sql/parts/notebook/models/contracts';
let expectedNotebookContent: nb.INotebookContents = {
cells: [{
cell_type: CellTypes.Code,
source: 'insert into t1 values (c1, c2)',
metadata: { language: 'python' },
execution_count: 1
}],
metadata: {
kernelspec: {
name: 'mssql',
language: 'sql'
}
},
nbformat: 5,
nbformat_minor: 0
};
let notebookContentString = JSON.stringify(expectedNotebookContent);
function verifyMatchesExpectedNotebook(notebook: nb.INotebookContents): void {
should(notebook.cells).have.length(1, 'Expected 1 cell');
should(notebook.cells[0].cell_type).equal(CellTypes.Code);
should(notebook.cells[0].source).equal(expectedNotebookContent.cells[0].source);
should(notebook.metadata.kernelspec.name).equal(expectedNotebookContent.metadata.kernelspec.name);
should(notebook.nbformat).equal(expectedNotebookContent.nbformat);
should(notebook.nbformat_minor).equal(expectedNotebookContent.nbformat_minor);
}
describe('Local Content Manager', function(): void {
let contentManager = new LocalContentManager();
it('Should return undefined if path is undefined', async function(): Promise<void> {
let content = await contentManager.getNotebookContents(undefined);
should(content).be.undefined();
// tslint:disable-next-line:no-null-keyword
content = await contentManager.getNotebookContents(null);
should(content).be.undefined();
});
it('Should throw if file does not exist', async function(): Promise<void> {
await testUtils.assertThrowsAsync(async () => await contentManager.getNotebookContents(URI.file('/path/doesnot/exist.ipynb')), undefined);
});
it('Should return notebook contents parsed as INotebook when valid notebook file parsed', async function(): Promise<void> {
// Given a file containing a valid notebook
let localFile = tempWrite.sync(notebookContentString, 'notebook.ipynb');
// when I read the content
let notebook = await contentManager.getNotebookContents(URI.file(localFile));
// then I expect notebook format to match
verifyMatchesExpectedNotebook(notebook);
});
it('Should ignore invalid content in the notebook file', async function(): Promise<void> {
// Given a file containing a notebook with some garbage properties
let invalidContent = notebookContentString + '\\nasddfdsafasdf';
let localFile = tempWrite.sync(invalidContent, 'notebook.ipynb');
// when I read the content
let notebook = await contentManager.getNotebookContents(URI.file(localFile));
// then I expect notebook format to still be valid
verifyMatchesExpectedNotebook(notebook);
});
});

View File

@@ -0,0 +1,251 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import { nb } from 'sqlops';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import URI from 'vs/base/common/uri';
import { LocalContentManager } from 'sql/services/notebook/localContentManager';
import * as testUtils from '../../../utils/testUtils';
import { NotebookManagerStub } from '../common';
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
import { ModelFactory } from 'sql/parts/notebook/models/modelFactory';
import { IClientSession, ICellModel, INotebookModelOptions } from 'sql/parts/notebook/models/modelInterfaces';
import { ClientSession } from 'sql/parts/notebook/models/clientSession';
import { CellTypes } from 'sql/parts/notebook/models/contracts';
import { Deferred } from 'sql/base/common/promise';
import { ConnectionManagementService } from 'sql/parts/connection/common/connectionManagementService';
import { Memento } from 'vs/workbench/common/memento';
import { Emitter } from 'vs/base/common/event';
let expectedNotebookContent: nb.INotebookContents = {
cells: [{
cell_type: CellTypes.Code,
source: 'insert into t1 values (c1, c2)',
metadata: { language: 'python' },
execution_count: 1
}, {
cell_type: CellTypes.Markdown,
source: 'I am *markdown*',
metadata: { language: 'python' },
execution_count: 1
}],
metadata: {
kernelspec: {
name: 'mssql',
language: 'sql'
}
},
nbformat: 5,
nbformat_minor: 0
};
let expectedNotebookContentOneCell: nb.INotebookContents = {
cells: [{
cell_type: CellTypes.Code,
source: 'insert into t1 values (c1, c2)',
metadata: { language: 'python' },
execution_count: 1
}],
metadata: {
kernelspec: {
name: 'mssql',
language: 'sql'
}
},
nbformat: 5,
nbformat_minor: 0
};
let defaultUri = URI.file('/some/path.ipynb');
let mockClientSession: TypeMoq.Mock<IClientSession>;
let sessionReady: Deferred<void>;
let mockModelFactory: TypeMoq.Mock<ModelFactory>;
let notificationService: TypeMoq.Mock<INotificationService>;
describe('notebook model', function(): void {
let notebookManager = new NotebookManagerStub();
let memento: TypeMoq.Mock<Memento>;
let queryConnectionService: TypeMoq.Mock<ConnectionManagementService>;
let defaultModelOptions: INotebookModelOptions;
beforeEach(() => {
sessionReady = new Deferred<void>();
notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose);
memento = TypeMoq.Mock.ofType(Memento, TypeMoq.MockBehavior.Loose, '');
memento.setup(x => x.getMemento(TypeMoq.It.isAny())).returns(() => void 0);
queryConnectionService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Loose, memento.object, undefined);
queryConnectionService.callBase = true;
defaultModelOptions = {
notebookUri: defaultUri,
factory: new ModelFactory(),
notebookManager,
notificationService: notificationService.object,
connectionService: queryConnectionService.object };
mockClientSession = TypeMoq.Mock.ofType(ClientSession, undefined, defaultModelOptions);
mockClientSession.setup(c => c.initialize(TypeMoq.It.isAny())).returns(() => {
return Promise.resolve();
});
mockClientSession.setup(c => c.ready).returns(() => sessionReady.promise);
mockModelFactory = TypeMoq.Mock.ofType(ModelFactory);
mockModelFactory.callBase = true;
mockModelFactory.setup(f => f.createClientSession(TypeMoq.It.isAny())).returns(() => {
return mockClientSession.object;
});
});
it('Should create single cell if model has no contents', async function(): Promise<void> {
// Given an empty notebook
let emptyNotebook: nb.INotebookContents = {
cells: [],
metadata: {
kernelspec: {
name: 'mssql',
language: 'sql'
}
},
nbformat: 5,
nbformat_minor: 0
};
let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager);
mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(emptyNotebook));
notebookManager.contentManager = mockContentManager.object;
// When I initialize the model
let model = new NotebookModel(defaultModelOptions);
await model.requestModelLoad();
// Then I expect to have 1 code cell as the contents
should(model.cells).have.length(1);
should(model.cells[0].source).be.empty();
});
it('Should throw if model load fails', async function(): Promise<void> {
// Given a call to get Contents fails
let error = new Error('File not found');
let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager);
mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).throws(error);
notebookManager.contentManager = mockContentManager.object;
// When I initalize the model
// Then it should throw
let model = new NotebookModel(defaultModelOptions);
should(model.inErrorState).be.false();
await testUtils.assertThrowsAsync(() => model.requestModelLoad(), error.message);
should(model.inErrorState).be.true();
});
it('Should convert cell info to CellModels', async function(): Promise<void> {
// Given a notebook with 2 cells
let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager);
mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContent));
notebookManager.contentManager = mockContentManager.object;
// When I initalize the model
let model = new NotebookModel(defaultModelOptions);
await model.requestModelLoad();
// Then I expect all cells to be in the model
should(model.cells).have.length(2);
should(model.cells[0].source).be.equal(expectedNotebookContent.cells[0].source);
should(model.cells[1].source).be.equal(expectedNotebookContent.cells[1].source);
});
it('Should load contents but then go to error state if client session startup fails', async function(): Promise<void> {
let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager);
mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContentOneCell));
notebookManager.contentManager = mockContentManager.object;
// Given I have a session that fails to start
mockClientSession.setup(c => c.isInErrorState).returns(() => true);
mockClientSession.setup(c => c.errorMessage).returns(() => 'Error');
sessionReady.resolve();
let sessionFired = false;
let options: INotebookModelOptions = Object.assign({}, defaultModelOptions, <Partial<INotebookModelOptions>> {
factory: mockModelFactory.object
});
let model = new NotebookModel(options);
model.onClientSessionReady((session) => sessionFired = true);
await model.requestModelLoad();
model.backgroundStartSession();
// Then I expect load to succeed
shouldHaveOneCell(model);
should(model.clientSession).not.be.undefined();
// but on server load completion I expect error state to be set
// Note: do not expect serverLoad event to throw even if failed
await model.sessionLoadFinished;
should(model.inErrorState).be.true();
should(sessionFired).be.false();
});
it('Should not be in error state if client session initialization succeeds', async function(): Promise<void> {
let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager);
mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContentOneCell));
notebookManager.contentManager = mockContentManager.object;
let kernelChangedEmitter: Emitter<nb.IKernelChangedArgs> = new Emitter<nb.IKernelChangedArgs>();
mockClientSession.setup(c => c.isInErrorState).returns(() => false);
mockClientSession.setup(c => c.isReady).returns(() => true);
mockClientSession.setup(c => c.kernelChanged).returns(() => kernelChangedEmitter.event);
queryConnectionService.setup(c => c.getActiveConnections(TypeMoq.It.isAny())).returns(() => null);
sessionReady.resolve();
let actualSession: IClientSession = undefined;
let options: INotebookModelOptions = Object.assign({}, defaultModelOptions, <Partial<INotebookModelOptions>> {
factory: mockModelFactory.object
});
let model = new NotebookModel(options, false);
model.onClientSessionReady((session) => actualSession = session);
await model.requestModelLoad();
model.backgroundStartSession();
// Then I expect load to succeed
should(model.clientSession).not.be.undefined();
// but on server load completion I expect error state to be set
// Note: do not expect serverLoad event to throw even if failed
let kernelChangedArg: nb.IKernelChangedArgs = undefined;
model.kernelChanged((kernel) => kernelChangedArg = kernel);
await model.sessionLoadFinished;
should(model.inErrorState).be.false();
should(actualSession).equal(mockClientSession.object);
should(model.clientSession).equal(mockClientSession.object);
});
it('Should sanitize kernel display name when IP is included', async function(): Promise<void> {
let model = new NotebookModel(defaultModelOptions);
let displayName = 'PySpark (1.1.1.1)';
let sanitizedDisplayName = model.sanitizeDisplayName(displayName);
should(sanitizedDisplayName).equal('PySpark');
});
it('Should sanitize kernel display name properly when IP is not included', async function(): Promise<void> {
let model = new NotebookModel(defaultModelOptions);
let displayName = 'PySpark';
let sanitizedDisplayName = model.sanitizeDisplayName(displayName);
should(sanitizedDisplayName).equal('PySpark');
});
function shouldHaveOneCell(model: NotebookModel): void {
should(model.cells).have.length(1);
verifyCellModel(model.cells[0], { cell_type: CellTypes.Code, source: 'insert into t1 values (c1, c2)', metadata: { language: 'python' }, execution_count: 1 });
}
function verifyCellModel(cellModel: ICellModel, expected: nb.ICellContents): void {
should(cellModel.cellType).equal(expected.cell_type);
should(cellModel.source).equal(expected.source);
}
});

View File

@@ -66,7 +66,7 @@ suite('MainThreadModelViewDialog Tests', () => {
let extHostContext = <IExtHostContext>{
getProxy: proxyType => mockExtHostModelViewDialog.object
};
mainThreadModelViewDialog = new MainThreadModelViewDialog(extHostContext, undefined, undefined);
mainThreadModelViewDialog = new MainThreadModelViewDialog(extHostContext, undefined, undefined, undefined);
// Set up the mock dialog service
mockDialogService = Mock.ofType(CustomDialogService, undefined, undefined);

View File

@@ -131,10 +131,10 @@ class ExtHostNotebookStub implements ExtHostNotebookShape {
$doStopServer(managerHandle: number): Thenable<void> {
throw new Error('Method not implemented.');
}
$getNotebookContents(managerHandle: number, notebookUri: UriComponents): Thenable<sqlops.nb.INotebook> {
$getNotebookContents(managerHandle: number, notebookUri: UriComponents): Thenable<sqlops.nb.INotebookContents> {
throw new Error('Method not implemented.');
}
$save(managerHandle: number, notebookUri: UriComponents, notebook: sqlops.nb.INotebook): Thenable<sqlops.nb.INotebook> {
$save(managerHandle: number, notebookUri: UriComponents, notebook: sqlops.nb.INotebookContents): Thenable<sqlops.nb.INotebookContents> {
throw new Error('Method not implemented.');
}
$refreshSpecs(managerHandle: number): Thenable<sqlops.nb.IAllKernels> {

238
src/typings/should.d.ts vendored Executable file
View File

@@ -0,0 +1,238 @@
// Type definitions for should.js
declare function should(obj: any): should.Assertion;
// node assert methods
/*interface NodeAssert {
fail(actual: any, expected: any, message?: string, operator?: string): void;
ok(value: any, message?: string): void;
equal(actual: any, expected: any, message?: string): void;
notEqual(actual: any, expected: any, message?: string): void;
deepEqual(actual: any, expected: any, message?: string): void;
notDeepEqual(actual: any, expected: any, message?: string): void;
strictEqual(actual: any, expected: any, message?: string): void;
notStrictEqual(actual: any, expected: any, message?: string): void;
throws(block: Function, message?: string): void;
throws(block: Function, error: Function, message?: string): void;
throws(block: Function, error: RegExp, message?: string): void;
throws(block: Function, error: (err: any) => boolean, message?: string): void;
doesNotThrow(block: Function, message?: string): void;
doesNotThrow(block: Function, error: Function, message?: string): void;
doesNotThrow(block: Function, error: RegExp, message?: string): void;
doesNotThrow(block: Function, error: (err: any) => boolean, message?: string): void;
ifError(value: any): void;
}
interface should extends NodeAssert, ShouldAssertExt {
not: ShouldAssertExt;
}*/
declare module should {
interface ShouldAssertExt {
exist(obj: any, msg?: string): void;
exists(obj: any, msg?: string): void;
}
function fail(actual: any, expected: any, message?: string, operator?: string): void;
function ok(value: any, message?: string): void;
function equal(actual: any, expected: any, message?: string): void;
function notEqual(actual: any, expected: any, message?: string): void;
function deepEqual(actual: any, expected: any, message?: string): void;
function notDeepEqual(actual: any, expected: any, message?: string): void;
function strictEqual(actual: any, expected: any, message?: string): void;
function notStrictEqual(actual: any, expected: any, message?: string): void;
function throws(block: Function, message?: string): void;
function throws(block: Function, error: Function, message?: string): void;
function throws(block: Function, error: RegExp, message?: string): void;
function throws(block: Function, error: (err: any) => boolean, message?: string): void;
function doesNotThrow(block: Function, message?: string): void;
function doesNotThrow(block: Function, error: Function, message?: string): void;
function doesNotThrow(block: Function, error: RegExp, message?: string): void;
function doesNotThrow(block: Function, error: (err: any) => boolean, message?: string): void;
function ifError(value: any): void;
function exist(obj: any, msg?: string): void;
function exists(obj: any, msg?: string): void;
const not: ShouldAssertExt;
interface Assertion {
assert(expr: boolean): this;
fail(): this;
not: this;
any: this;
only: this;
// bool
true(message?: string): this;
True(message?: string): this;
false(message?: string): this;
False(message?: string): this;
ok(): this;
//chain
an: this;
of: this;
a: this;
and: this;
be: this;
been: this;
has: this;
have: this;
with: this;
is: this;
which: this;
the: this;
it: this;
//contain
containEql(obj: any): this;
containDeepOrdered(obj: any): this;
containDeep(obj: any): this;
// eql
eql(obj: any, description?: string): this;
eqls(obj: any, description?: string): this;
deepEqual(obj: any, description?: string): this;
equal(obj: any, description?: string): this;
equals(obj: any, description?: string): this;
exactly(obj: any, description?: string): this;
equalOneOf(...objs: any[]): this;
equalOneOf(obj: any[]): this;
oneOf(...objs: any[]): this;
oneOf(obj: any[]): this;
//error
throw(): this;
throw(msg: RegExp | string | Function, properties?: {}): this;
throw(properties: {}): this;
//TODO how to express generators???
throwError(): this;
throwError(msg: RegExp | string | Function, properties?: {}): this;
throwError(properties: {}): this;
// match
match(
obj: RegExp | ((value: any, key: any) => boolean) | ((value: any, key: any) => void) | {},
description?: string
): this;
matchEach(
obj: RegExp | ((value: any, key: any) => boolean) | ((value: any, key: any) => void) | {},
description?: string
): this;
matchEvery(
obj: RegExp | ((value: any, key: any) => boolean) | ((value: any, key: any) => void) | {},
description?: string
): this;
matchAny(
obj: RegExp | ((value: any, key: any) => boolean) | ((value: any, key: any) => void) | {},
description?: string
): this;
matchSome(
obj: RegExp | ((value: any, key: any) => boolean) | ((value: any, key: any) => void) | {},
description?: string
): this;
//number
NaN(): this;
Infinity(): this;
within(start: number, finish: number, description?: string): this;
approximately(value: number, delta: number, description?: string): this;
above(value: number, description?: string): this;
greaterThan(value: number, description?: string): this;
below(value: number, description?: string): this;
lessThan(value: number, description?: string): this;
aboveOrEqual(value: number, description?: string): this;
greaterThanOrEqual(value: number, description?: string): this;
belowOrEqual(value: number, description?: string): this;
lessThanOrEqual(value: number, description?: string): this;
//promise
Promise(): this;
fulfilled(): Promise<any>;
resolved(): Promise<any>;
rejected(): Promise<any>;
fulfilledWith(obj: any): Promise<any>;
resolvedWith(obj: any): Promise<any>;
rejectedWith(msg: RegExp | string | Error, properties?: {}): Promise<any>;
rejectedWith(properties: {}): Promise<any>;
finally: PromisedAssertion;
eventually: PromisedAssertion;
// property
propertyWithDescriptor(name: string, descriptor: {}): this;
property(name: string, value?: any): this;
properties(...names: string[]): this;
properties(names: string[]): this;
properties(props: {}): this;
length(value: number, description?: string): this;
lengthOf(value: number, description?: string): this;
ownProperty(name: string, description?: string): this;
hasOwnProperty(name: string, description?: string): this;
empty(): this;
keys(...keys: any[]): this;
key(key: any): this;
value(key: any, value: any): this;
size(value: number): this;
propertyByPath(...path: string[]): this;
propertyByPath(path: string[]): this;
//string
startWith(prefix: string, description?: string): this;
endWith(postfix: string, description?: string): this;
//type
Number(): this;
arguments(): this;
Arguments(): this;
type(typeName: string, description?: string): this;
instanceof(constructor: Function, description?: string): this;
instanceOf(constructor: Function, description?: string): this;
Function(): this;
Object(): this;
String(): this;
Array(): this;
Boolean(): this;
Error(): this;
Date(): this;
null(): this;
Null(): this;
class(className: string): this;
Class(className: string): this;
undefined(): this;
Undefined(): this;
iterable(): this;
iterator(): this;
generator(): this;
}
interface PromisedAssertion extends Assertion, PromiseLike<any> {}
}
declare module 'should' {
export = should;
}

3
src/typings/temp-write.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare module 'temp-write' {
function sync(input: string, filePath?: string): string;
}

View File

@@ -99,6 +99,13 @@
version "5.5.0"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45"
"@types/should@^13.0.0":
version "13.0.0"
resolved "https://registry.yarnpkg.com/@types/should/-/should-13.0.0.tgz#96c00117f1896177848fdecfa336313c230c879e"
integrity sha512-Mi6YZ2ABnnGGFMuiBDP0a8s1ZDCDNHqP97UH8TyDmCWuGGavpsFMfJnAMYaaqmDlSCOCNbVLHBrSDEOpx/oLhw==
dependencies:
should "*"
"@types/sinon@1.16.34":
version "1.16.34"
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-1.16.34.tgz#a9761fff33d0f7b3fe61875b577778a2576a9a03"
@@ -4373,6 +4380,13 @@ macaddress@^0.2.8:
version "0.2.8"
resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
make-dir@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==
dependencies:
pify "^3.0.0"
make-error-cause@^1.1.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d"
@@ -5265,6 +5279,11 @@ pify@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
pify@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
pinkie-promise@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
@@ -6350,6 +6369,50 @@ shelljs@^0.7.5:
interpret "^1.0.0"
rechoir "^0.6.2"
should-equal@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3"
integrity sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==
dependencies:
should-type "^1.4.0"
should-format@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/should-format/-/should-format-3.0.3.tgz#9bfc8f74fa39205c53d38c34d717303e277124f1"
integrity sha1-m/yPdPo5IFxT04w01xcwPidxJPE=
dependencies:
should-type "^1.3.0"
should-type-adaptors "^1.0.1"
should-type-adaptors@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz#401e7f33b5533033944d5cd8bf2b65027792e27a"
integrity sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==
dependencies:
should-type "^1.3.0"
should-util "^1.0.0"
should-type@^1.3.0, should-type@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/should-type/-/should-type-1.4.0.tgz#0756d8ce846dfd09843a6947719dfa0d4cff5cf3"
integrity sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=
should-util@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/should-util/-/should-util-1.0.0.tgz#c98cda374aa6b190df8ba87c9889c2b4db620063"
integrity sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM=
should@*, should@^13.2.3:
version "13.2.3"
resolved "https://registry.yarnpkg.com/should/-/should-13.2.3.tgz#96d8e5acf3e97b49d89b51feaa5ae8d07ef58f10"
integrity sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==
dependencies:
should-equal "^2.0.0"
should-format "^3.0.3"
should-type "^1.4.0"
should-type-adaptors "^1.0.1"
should-util "^1.0.0"
sigmund@^1.0.1, sigmund@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
@@ -6804,6 +6867,23 @@ tar-stream@^1.1.2:
to-buffer "^1.1.0"
xtend "^4.0.0"
temp-dir@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d"
integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=
temp-write@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-3.4.0.tgz#8cff630fb7e9da05f047c74ce4ce4d685457d492"
integrity sha1-jP9jD7fp2gXwR8dM5M5NaFRX1JI=
dependencies:
graceful-fs "^4.1.2"
is-stream "^1.1.0"
make-dir "^1.0.0"
pify "^3.0.0"
temp-dir "^1.0.0"
uuid "^3.0.1"
temp@^0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59"
@@ -7228,7 +7308,7 @@ uuid@^3.0.0, uuid@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
uuid@^3.3.2:
uuid@^3.0.1, uuid@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"