mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-20 02:51:36 -05:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a30878599 | ||
|
|
c8a8935db0 | ||
|
|
ec196f57bb | ||
|
|
f7809ec3a7 | ||
|
|
71d3ec3616 | ||
|
|
4a7cf8d870 | ||
|
|
4bf8836c0a | ||
|
|
1ca36ee29c | ||
|
|
3446ff88cf | ||
|
|
de5a91a13f | ||
|
|
814cd73019 | ||
|
|
c21611661b | ||
|
|
8f817ce689 | ||
|
|
971b5111e7 | ||
|
|
07069a64ae | ||
|
|
6acea51f12 | ||
|
|
7aa2dab307 | ||
|
|
3091be8f67 | ||
|
|
487531cc52 | ||
|
|
58bfcb4273 | ||
|
|
8d8be27f22 | ||
|
|
27a978cba5 | ||
|
|
71b4e6afa4 | ||
|
|
e1f3b19c0c | ||
|
|
649c2aa5a6 | ||
|
|
cac8cc99e1 | ||
|
|
cb162b16f2 | ||
|
|
86e54ce145 |
@@ -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",
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
17
extensions/notebook/README.md
Normal file
17
extensions/notebook/README.md
Normal 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).
|
||||
71
extensions/notebook/package.json
Normal file
71
extensions/notebook/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
8
extensions/notebook/package.nls.json
Normal file
8
extensions/notebook/package.nls.json
Normal 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"
|
||||
}
|
||||
1
extensions/notebook/resources/dark/new_notebook_inverse.svg
Executable file
1
extensions/notebook/resources/dark/new_notebook_inverse.svg
Executable 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 |
1
extensions/notebook/resources/dark/open_notebook_inverse.svg
Executable file
1
extensions/notebook/resources/dark/open_notebook_inverse.svg
Executable 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 |
1
extensions/notebook/resources/light/new_notebook.svg
Executable file
1
extensions/notebook/resources/light/new_notebook.svg
Executable 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 |
1
extensions/notebook/resources/light/open_notebook.svg
Executable file
1
extensions/notebook/resources/light/open_notebook.svg
Executable 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 |
50
extensions/notebook/src/extension.ts
Normal file
50
extensions/notebook/src/extension.ts
Normal 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() {
|
||||
}
|
||||
9
extensions/notebook/src/typings/refs.d.ts
vendored
Normal file
9
extensions/notebook/src/typings/refs.d.ts
vendored
Normal 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'/>
|
||||
22
extensions/notebook/tsconfig.json
Normal file
22
extensions/notebook/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
13
extensions/notebook/yarn.lock
Normal file
13
extensions/notebook/yarn.lock
Normal 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==
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -128,3 +128,7 @@
|
||||
.hide-refresh-link .azure-account-row.refresh-credentials-link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hide-azure-tenants .azure-tenant-row {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,5 +78,6 @@
|
||||
}
|
||||
|
||||
jobstepsview-component {
|
||||
padding-top: 10px;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,6 +282,7 @@ export class QueryResultsView extends Disposable {
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
dispose(this.runnerDisposables);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
20
src/sql/sqlops.d.ts
vendored
@@ -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 {
|
||||
|
||||
307
src/sql/sqlops.proposed.d.ts
vendored
307
src/sql/sqlops.proposed.d.ts
vendored
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
101
src/sql/workbench/api/node/extHostNotebookDocumentData.ts
Normal file
101
src/sql/workbench/api/node/extHostNotebookDocumentData.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
192
src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts
Normal file
192
src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts
Normal 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
|
||||
}
|
||||
210
src/sql/workbench/api/node/extHostNotebookEditor.ts
Normal file
210
src/sql/workbench/api/node/extHostNotebookEditor.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
99
src/sqltest/parts/notebook/common.ts
Normal file
99
src/sqltest/parts/notebook/common.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
262
src/sqltest/parts/notebook/model/cell.test.ts
Normal file
262
src/sqltest/parts/notebook/model/cell.test.ts
Normal 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());
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
202
src/sqltest/parts/notebook/model/clientSession.test.ts
Normal file
202
src/sqltest/parts/notebook/model/clientSession.test.ts
Normal 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();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
77
src/sqltest/parts/notebook/model/contentManagers.test.ts
Normal file
77
src/sqltest/parts/notebook/model/contentManagers.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
251
src/sqltest/parts/notebook/model/notebookModel.test.ts
Normal file
251
src/sqltest/parts/notebook/model/notebookModel.test.ts
Normal 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);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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
238
src/typings/should.d.ts
vendored
Executable 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
3
src/typings/temp-write.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare module 'temp-write' {
|
||||
function sync(input: string, filePath?: string): string;
|
||||
}
|
||||
82
yarn.lock
82
yarn.lock
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user