mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 11:01:37 -05:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84009f65ec | ||
|
|
191648df0a | ||
|
|
1265a56ff4 | ||
|
|
2171d44922 | ||
|
|
812f315e69 | ||
|
|
d48258a0fb | ||
|
|
8ec78f67af | ||
|
|
0ac0175bb1 | ||
|
|
f39007cd2d | ||
|
|
348b03327e | ||
|
|
2349aa4df8 | ||
|
|
6497bbcc38 | ||
|
|
a93a173183 | ||
|
|
a1abca26df | ||
|
|
42e55dd2dd | ||
|
|
ca3146d38f | ||
|
|
7f6cd514a5 | ||
|
|
88e24e92b5 | ||
|
|
8b447e361f | ||
|
|
a92dd2d4e4 | ||
|
|
852ec44567 | ||
|
|
b6e32cdeb4 | ||
|
|
4bd264d9be | ||
|
|
4a4b8574d0 | ||
|
|
ded073edd9 | ||
|
|
568f95e7a3 | ||
|
|
5adcabc8de | ||
|
|
e3bce7172c | ||
|
|
96fb618390 | ||
|
|
2d4fdcb661 | ||
|
|
7a84cff5b4 | ||
|
|
2af627b704 | ||
|
|
77fdf18686 | ||
|
|
944a77fe42 | ||
|
|
049678b32e | ||
|
|
3325e4d854 | ||
|
|
1e90e88d4b | ||
|
|
8aeb33c98c | ||
|
|
3b08721835 | ||
|
|
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 |
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,6 +1,10 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please search existing issues to avoid creating duplicates. -->
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution or feature you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -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",
|
||||
|
||||
@@ -69,7 +69,7 @@ export class AlertData implements IAgentDialogData {
|
||||
this.eventDescriptionKeyword = alertInfo.eventDescriptionKeyword;
|
||||
this.eventSource = alertInfo.eventSource;
|
||||
this.hasNotification = alertInfo.hasNotification;
|
||||
this.includeEventDescription = alertInfo.includeEventDescription.toString();
|
||||
this.includeEventDescription = alertInfo.includeEventDescription ? alertInfo.includeEventDescription.toString() : null;
|
||||
this.isEnabled = alertInfo.isEnabled;
|
||||
this.jobId = alertInfo.jobId;
|
||||
this.lastOccurrenceDate = alertInfo.lastOccurrenceDate;
|
||||
@@ -82,7 +82,7 @@ export class AlertData implements IAgentDialogData {
|
||||
this.databaseName = alertInfo.databaseName;
|
||||
this.countResetDate = alertInfo.countResetDate;
|
||||
this.categoryName = alertInfo.categoryName;
|
||||
this.alertType = alertInfo.alertType.toString();
|
||||
this.alertType = alertInfo.alertType ? alertInfo.alertType.toString() : null;
|
||||
this.wmiEventNamespace = alertInfo.wmiEventNamespace;
|
||||
this.wmiEventQuery = alertInfo.wmiEventQuery;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,50 @@
|
||||
# Microsoft SQL Server Import for Azure Data Studio
|
||||
|
||||
Microsoft SQL Server Import for Azure Data Studio is a simple way to copy data from a flat file (.csv, .txt, .json) to a SQL Server table. Checkout below the reasons for using the Import Flat File wizard, how to find this wizard, and a simple example.
|
||||
Microsoft SQL Server Import for Azure Data Studio includes two wizards:
|
||||
- [Import Flat File Wizard](#import-flat-file-wizard-preview)
|
||||
- [Data-tier Application Wizard.](#data-tier-application-wizard-preview)
|
||||
|
||||
## Import Flat File Wizard *(preview)*
|
||||
**The Import Flat File Wizard** is a simple way to copy data from a flat file (.csv, .txt, .json) to a SQL Server table. Checkout below the reasons for using the Import Flat File wizard, how to find this wizard, and a simple example.
|
||||
|
||||
This experience is currently in its initial preview. Please report issues and feature requests [here.](https://github.com/microsoft/azuredatastudio/issues)
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/30873802/43433347-c958ed28-942b-11e8-8bbc-f4f2529c3978.png" width="800px" />
|
||||
|
||||
### Requirements
|
||||
* This wizard requires an active connection to a SQL Server instance to start.
|
||||
* This wizard only works on .txt and .csv files.
|
||||
|
||||
### How do I start the Import Flat File wizard?
|
||||
* The main entry point for the wizard is to right click a database in the Object Explorer, and click **Import wizard**.
|
||||
* If a user is connected to a SQL Server instance, the user can also press **Ctrl**+**I** to start the wizard.
|
||||
|
||||
### Why would I use the Import Flat File wizard?
|
||||
This wizard was created to improve the current import experience leveraging an intelligent framework known as Program Synthesis using Examples ([PROSE](https://microsoft.github.io/prose/)). For a user without specialized domain knowledge, importing data can often be a complex, error prone, and tedious task. This wizard streamlines the import process as simple as selecting an input file and unique table name, and the PROSE framework handles the rest.
|
||||
|
||||
PROSE analyzes data patterns in your input file to infer column names, types, delimiters, and more. This framework learns the structure of the file and does all of the hard work so users don't have to.
|
||||
|
||||
Please note that the PROSE binary components used by this extension are licensed under the [MICROSOFT SQL TOOLS IMPORT FLAT FILE EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/extensions/import/Microsoft_SQL_Server_Import_Extension_and_Tools_Import_Flat_File_Preview.docx).
|
||||
|
||||
## Data-tier Application Wizard *(preview)*
|
||||
**The Data-tier Application Wizard** provides an easy to use experience to deploy and extract .dacpac files and import and export .bacpac files.
|
||||
|
||||
This experience is currently in its initial preview. Please report issues and feature requests [here.](https://github.com/microsoft/azuredatastudio/issues)
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/30873802/49676289-f2df6880-fa2d-11e8-8bfa-6213b7734075.png" width="800px" />
|
||||
|
||||
### Requirements
|
||||
* This wizard requires an active connection to a SQL Server instance to start.
|
||||
|
||||
### How do I start the Data-tier Application wizard?
|
||||
* The main entry point for the wizard is to right click a database in the Object Explorer, and click **Data-tier Application wizard**.
|
||||
* If a user is connected to a SQL Server instance, the user can also start the wizard from the command palette (Ctrl+Shift+P) by searching for **Data-tier Application wizard.**
|
||||
|
||||
### Why would I use the Data-tier Application wizard?
|
||||
This wizard was created to add the ability to extract and deploy .dacpac files and import and export .bacpac files in Azure Data Studio.
|
||||
|
||||
To learn more about Data-Tier Applications and working with dacpac and bacpac files, [you can read more here.](https://docs.microsoft.com/en-us/sql/relational-databases/data-tier-applications/data-tier-applications?view=sql-server-2017)
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
@@ -12,21 +53,6 @@ Licensed under the [MICROSOFT SQL SERVER IMPORT EXTENSION EULA](https://raw.gith
|
||||
|
||||
> Note: Microsoft SQL Server Import for Azure Data Studio extension contains the Microsoft SQL Tools Import Flat File component which is also licensed under the above EULA.
|
||||
|
||||
## Requirements
|
||||
* This wizard requires an active connection to a SQL Server instance to start.
|
||||
* This wizard only works on .txt and .csv files.
|
||||
|
||||
## How do I start the Flat File Import wizard?
|
||||
* The main entry point for the wizard is to right click a database in the Object Explorer, and click **Import wizard**.
|
||||
* If a user is connected to a SQL Server instance, the user can also press **Ctrl**+**I** to start the wizard.
|
||||
|
||||
## Why would I use the Flat File Import wizard?
|
||||
This wizard was created to improve the current import experience leveraging an intelligent framework known as Program Synthesis using Examples ([PROSE](https://microsoft.github.io/prose/)). For a user without specialized domain knowledge, importing data can often be a complex, error prone, and tedious task. This wizard streamlines the import process as simple as selecting an input file and unique table name, and the PROSE framework handles the rest.
|
||||
|
||||
PROSE analyzes data patterns in your input file to infer column names, types, delimiters, and more. This framework learns the structure of the file and does all of the hard work so users don't have to.
|
||||
|
||||
Please note that the PROSE binary components used by this extension are licensed under the [MICROSOFT SQL TOOLS IMPORT FLAT FILE EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/extensions/import/Microsoft_SQL_Server_Import_Extension_and_Tools_Import_Flat_File_Preview.docx).
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -202,7 +202,7 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
|
||||
private async deploy() {
|
||||
let service = await DataTierApplicationWizard.getService();
|
||||
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
|
||||
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
|
||||
|
||||
let result = await service.deployDacpac(this.model.filePath, this.model.database, this.model.upgradeExisting, ownerUri, sqlops.TaskExecutionMode.execute);
|
||||
@@ -213,7 +213,7 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
|
||||
private async extract() {
|
||||
let service = await DataTierApplicationWizard.getService();
|
||||
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
|
||||
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
|
||||
|
||||
let result = await service.extractDacpac(this.model.database, this.model.filePath, this.model.database, this.model.version, ownerUri, sqlops.TaskExecutionMode.execute);
|
||||
@@ -224,7 +224,7 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
|
||||
private async export() {
|
||||
let service = await DataTierApplicationWizard.getService();
|
||||
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
|
||||
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
|
||||
|
||||
let result = await service.exportBacpac(this.model.database, this.model.filePath, ownerUri, sqlops.TaskExecutionMode.execute);
|
||||
@@ -235,7 +235,7 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
|
||||
private async import() {
|
||||
let service = await DataTierApplicationWizard.getService();
|
||||
let service = await DataTierApplicationWizard.getService(this.model.server.providerName);
|
||||
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
|
||||
|
||||
let result = await service.importBacpac(this.model.filePath, this.model.database, ownerUri, sqlops.TaskExecutionMode.execute);
|
||||
@@ -245,9 +245,8 @@ export class DataTierApplicationWizard {
|
||||
}
|
||||
}
|
||||
|
||||
public static async getService(): Promise<sqlops.DacFxServicesProvider> {
|
||||
let currentConnection = await sqlops.connection.getCurrentConnection();
|
||||
let service = sqlops.dataprotocol.getProvider<sqlops.DacFxServicesProvider>(currentConnection.providerName, sqlops.DataProviderType.DacFxServicesProvider);
|
||||
private static async getService(providerName: string): Promise<sqlops.DacFxServicesProvider> {
|
||||
let service = sqlops.dataprotocol.getProvider<sqlops.DacFxServicesProvider>(providerName, sqlops.DataProviderType.DacFxServicesProvider);
|
||||
return service;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export class DeployConfigPage extends DacFxConfigPage {
|
||||
}
|
||||
|
||||
async start(): Promise<boolean> {
|
||||
let serverComponent = await this.createServerDropdown(true);
|
||||
let serverComponent = await this.createServerDropdown(true);
|
||||
let fileBrowserComponent = await this.createFileBrowser();
|
||||
this.databaseComponent = await this.createDatabaseTextBox();
|
||||
this.databaseComponent.title = localize('dacFx.databaseNameTextBox', 'Database Name');
|
||||
@@ -131,7 +131,7 @@ export class DeployConfigPage extends DacFxConfigPage {
|
||||
this.model.database = this.databaseTextBox.value;
|
||||
});
|
||||
|
||||
// Initialize with upgrade existing true
|
||||
//Initialize with upgrade existing true
|
||||
upgradeRadioButton.checked = true;
|
||||
this.model.upgradeExisting = true;
|
||||
|
||||
@@ -149,10 +149,10 @@ export class DeployConfigPage extends DacFxConfigPage {
|
||||
}
|
||||
|
||||
protected async createDeployDatabaseDropdown(): Promise<sqlops.FormComponent> {
|
||||
this.databaseDropdown = this.view.modelBuilder.dropDown().withProperties({
|
||||
this.databaseDropdown = this.view.modelBuilder.dropDown().withProperties({
|
||||
required: true
|
||||
}).component();
|
||||
// Handle database changes
|
||||
//Handle database changes
|
||||
this.databaseDropdown.onValueChanged(async () => {
|
||||
this.model.database = (<sqlops.CategoryValue>this.databaseDropdown.value).name;
|
||||
});
|
||||
@@ -172,7 +172,8 @@ export class DeployConfigPage extends DacFxConfigPage {
|
||||
}
|
||||
let values = await this.getDatabaseValues();
|
||||
|
||||
if (this.model.database === undefined) {
|
||||
//set the database to the first dropdown value if upgrading, otherwise it should get set to the textbox value
|
||||
if (this.model.upgradeExisting) {
|
||||
this.model.database = values[0].name;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.8",
|
||||
"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",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDisposableDataProvider } from 'sql/base/browser/ui/table/interfaces';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
|
||||
export interface IObservableCollection<T> {
|
||||
getLength(): number;
|
||||
@@ -14,16 +15,12 @@ export interface IObservableCollection<T> {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
class LoadCancellationToken {
|
||||
isCancelled: boolean;
|
||||
}
|
||||
|
||||
class DataWindow<T> {
|
||||
private _data: T[];
|
||||
private _length: number = 0;
|
||||
private _offsetFromDataSource: number = -1;
|
||||
|
||||
private lastLoadCancellationToken: LoadCancellationToken;
|
||||
private cancellationToken = new CancellationTokenSource();
|
||||
|
||||
constructor(
|
||||
private loadFunction: (offset: number, count: number) => Thenable<T[]>,
|
||||
@@ -36,9 +33,7 @@ class DataWindow<T> {
|
||||
this.loadFunction = undefined;
|
||||
this.placeholderItemGenerator = undefined;
|
||||
this.loadCompleteCallback = undefined;
|
||||
if (this.lastLoadCancellationToken) {
|
||||
this.lastLoadCancellationToken.isCancelled = true;
|
||||
}
|
||||
this.cancellationToken.cancel();
|
||||
}
|
||||
|
||||
public getStartIndex(): number {
|
||||
@@ -65,17 +60,16 @@ class DataWindow<T> {
|
||||
this._length = length;
|
||||
this._data = undefined;
|
||||
|
||||
if (this.lastLoadCancellationToken) {
|
||||
this.lastLoadCancellationToken.isCancelled = true;
|
||||
}
|
||||
this.cancellationToken.cancel();
|
||||
this.cancellationToken = new CancellationTokenSource();
|
||||
const currentCancellation = this.cancellationToken;
|
||||
|
||||
if (length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastLoadCancellationToken = new LoadCancellationToken();
|
||||
this.loadFunction(offset, length).then(data => {
|
||||
if (!this.lastLoadCancellationToken.isCancelled) {
|
||||
if (!currentCancellation.token.isCancellationRequested) {
|
||||
this._data = data;
|
||||
this.loadCompleteCallback(this._offsetFromDataSource, this._offsetFromDataSource + this._length);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export class TableDataView<T extends Slick.SlickData> implements IDisposableData
|
||||
this._onRowCountChange.fire();
|
||||
}
|
||||
|
||||
find(exp: string): Thenable<IFindPosition> {
|
||||
find(exp: string, maxMatches: number = 0): Thenable<IFindPosition> {
|
||||
if (!this._findFn) {
|
||||
return TPromise.wrapError(new Error('no find function provided'));
|
||||
}
|
||||
@@ -87,7 +87,8 @@ export class TableDataView<T extends Slick.SlickData> implements IDisposableData
|
||||
this._onFindCountChange.fire(this._findArray.length);
|
||||
if (exp) {
|
||||
this._findObs = Observable.create((observer: Observer<IFindPosition>) => {
|
||||
this._data.forEach((item, i) => {
|
||||
for (let i = 0; i < this._data.length; i++) {
|
||||
let item = this._data[i];
|
||||
let result = this._findFn(item, exp);
|
||||
if (result) {
|
||||
result.forEach(pos => {
|
||||
@@ -96,8 +97,11 @@ export class TableDataView<T extends Slick.SlickData> implements IDisposableData
|
||||
observer.next(index);
|
||||
this._onFindCountChange.fire(this._findArray.length);
|
||||
});
|
||||
if (maxMatches > 0 && this._findArray.length > maxMatches) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return this._findObs.take(1).toPromise().then(() => {
|
||||
return this._findArray[this._findIndex];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
|
||||
import * as path from 'path';
|
||||
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { EditorInput, IEditorInput } from 'vs/workbench/common/editor';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||
import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput';
|
||||
import URI from 'vs/base/common/uri';
|
||||
@@ -17,8 +16,9 @@ import { QueryInput } from 'sql/parts/query/common/queryInput';
|
||||
import { IQueryEditorOptions } from 'sql/parts/query/common/queryEditorService';
|
||||
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 { DEFAULT_NOTEBOOK_PROVIDER, INotebookService } from 'sql/services/notebook/notebookService';
|
||||
import { getProviderForFileName } from 'sql/parts/notebook/notebookUtils';
|
||||
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
@@ -29,6 +29,7 @@ export const untitledFilePrefix = 'SQLQuery';
|
||||
|
||||
// mode identifier for SQL mode
|
||||
export const sqlModeId = 'sql';
|
||||
export const notebookModeId = 'notebook';
|
||||
|
||||
/**
|
||||
* Checks if the specified input is supported by one our custom input types, and if so convert it
|
||||
@@ -58,20 +59,20 @@ export function convertEditorInput(input: EditorInput, options: IQueryEditorOpti
|
||||
|
||||
//Notebook
|
||||
let notebookValidator = instantiationService.createInstance(NotebookInputValidator);
|
||||
uri = getNotebookEditorUri(input);
|
||||
if(uri && notebookValidator.isNotebookEnabled()){
|
||||
//TODO: We need to pass in notebook data either through notebook input or notebook service
|
||||
let fileName: string = 'untitled';
|
||||
let providerId: string = DEFAULT_NOTEBOOK_PROVIDER;
|
||||
if (input) {
|
||||
fileName = input.getName();
|
||||
providerId = getProviderForFileName(fileName);
|
||||
}
|
||||
let notebookInputModel = new NotebookInputModel(uri, undefined, false, undefined);
|
||||
notebookInputModel.providerId = providerId;
|
||||
//TO DO: Second parameter has to be the content.
|
||||
let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel);
|
||||
return notebookInput;
|
||||
uri = getNotebookEditorUri(input, instantiationService);
|
||||
if (uri && notebookValidator.isNotebookEnabled()) {
|
||||
return withService<INotebookService, NotebookInput>(instantiationService, INotebookService, notebookService => {
|
||||
let fileName: string = 'untitled';
|
||||
let providerId: string = DEFAULT_NOTEBOOK_PROVIDER;
|
||||
if (input) {
|
||||
fileName = input.getName();
|
||||
providerId = getProviderForFileName(fileName, notebookService);
|
||||
}
|
||||
let notebookInputModel = new NotebookInputModel(uri, undefined, false, undefined);
|
||||
notebookInputModel.providerId = providerId;
|
||||
let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel);
|
||||
return notebookInput;
|
||||
});
|
||||
}
|
||||
}
|
||||
return input;
|
||||
@@ -96,6 +97,13 @@ export function getSupportedInputResource(input: IEditorInput): URI {
|
||||
}
|
||||
}
|
||||
|
||||
if (input instanceof ResourceEditorInput) {
|
||||
let resourceCast: ResourceEditorInput = <ResourceEditorInput>input;
|
||||
if (resourceCast) {
|
||||
return resourceCast.getResource();
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -153,46 +161,51 @@ function getQueryPlanEditorUri(input: EditorInput): URI {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If input is a supported notebook editor file (.ipynb), return it's URI. Otherwise return undefined.
|
||||
* @param input The EditorInput to get the URI of.
|
||||
*/
|
||||
function getNotebookEditorUri(input: EditorInput): URI {
|
||||
function getNotebookEditorUri(input: EditorInput, instantiationService: IInstantiationService): URI {
|
||||
if (!input || !input.getName()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// If this editor is not already of type notebook input
|
||||
if (!(input instanceof NotebookInput)) {
|
||||
let uri: URI = getSupportedInputResource(input);
|
||||
if (uri) {
|
||||
if (hasFileExtension(getNotebookFileExtensions(), input, false)) {
|
||||
if (hasFileExtension(getNotebookFileExtensions(instantiationService), input, false) || hasNotebookFileMode(input)) {
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getNotebookFileExtensions() {
|
||||
let notebookRegistry = Registry.as<INotebookProviderRegistry>(Extensions.NotebookProviderContribution);
|
||||
return notebookRegistry.getSupportedFileExtensions();
|
||||
function getNotebookFileExtensions(instantiationService: IInstantiationService): string[] {
|
||||
return withService<INotebookService, string[]>(instantiationService, INotebookService, notebookService => {
|
||||
return notebookService.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);
|
||||
/**
|
||||
* Checks whether the given EditorInput is set to either undefined or notebook mode
|
||||
* @param input The EditorInput to check the mode of
|
||||
*/
|
||||
function hasNotebookFileMode(input: EditorInput): boolean {
|
||||
if (input instanceof UntitledEditorInput) {
|
||||
let untitledCast: UntitledEditorInput = <UntitledEditorInput>input;
|
||||
return (untitledCast && untitledCast.getModeId() === notebookModeId);
|
||||
}
|
||||
return DEFAULT_NOTEBOOK_PROVIDER;
|
||||
return false;
|
||||
}
|
||||
|
||||
function withService<TService, TResult>(instantiationService: IInstantiationService, serviceId: ServiceIdentifier<TService>, action: (service: TService) => TResult, ): TResult {
|
||||
return instantiationService.invokeFunction(accessor => {
|
||||
let service = accessor.get(serviceId);
|
||||
return action(service);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given EditorInput is set to either undefined or sql mode
|
||||
@@ -229,3 +242,17 @@ function hasFileExtension(extensions: string[], input: EditorInput, checkUntitle
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns file mode - notebookModeId or sqlModeId
|
||||
export function getFileMode(instantiationService: IInstantiationService, resource: URI): string {
|
||||
if (!resource) {
|
||||
return sqlModeId;
|
||||
}
|
||||
return withService<INotebookService, string>(instantiationService, INotebookService, notebookService => {
|
||||
for (const editor of notebookService.listNotebookEditors()) {
|
||||
if (editor.notebookParams.notebookUri === resource) {
|
||||
return notebookModeId;
|
||||
}
|
||||
}
|
||||
return sqlModeId;
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -105,10 +105,12 @@
|
||||
background: url('clear-search-results.svg');
|
||||
}
|
||||
|
||||
/*
|
||||
.vs-dark .search-action.clear-search-results,
|
||||
.hc-black .search-action.clear-search-results {
|
||||
background: url('clear-search-results-dark.svg');
|
||||
}
|
||||
*/
|
||||
|
||||
.connection-details-title {
|
||||
font-size: 14px;
|
||||
@@ -128,3 +130,7 @@
|
||||
.hide-refresh-link .azure-account-row.refresh-credentials-link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hide-azure-tenants .azure-tenant-row {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -168,8 +168,7 @@ export const DashboardModule = (params, selector: string, instantiationService:
|
||||
if (e instanceof NavigationEnd) {
|
||||
this.navigations++;
|
||||
TelemetryUtils.addTelemetry(this.telemetryService, TelemetryKeys.DashboardNavigated, {
|
||||
numberOfNavigations: this.navigations,
|
||||
routeUrl: e.url
|
||||
numberOfNavigations: this.navigations
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -343,6 +343,9 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On
|
||||
handleResultSet(self: EditDataComponent, event: any): void {
|
||||
// Clone the data before altering it to avoid impacting other subscribers
|
||||
let resultSet = Object.assign({}, event.data);
|
||||
if (!resultSet.complete) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add an extra 'new row'
|
||||
resultSet.rowCount++;
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -304,14 +304,6 @@ table.jobprevruns > tbody {
|
||||
background-image: url('refresh_inverse.svg');
|
||||
}
|
||||
|
||||
.agent-actionbar-container .monaco-action-bar > ul.actions-container {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
jobsview-component .agent-actionbar-container {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.agent-actionbar-container .monaco-action-bar > ul.actions-container > li.action-item {
|
||||
padding-left: 20px;
|
||||
}
|
||||
@@ -414,4 +406,11 @@ jobsview-component .jobview-grid .slick-cell.error-row {
|
||||
#proxiesDiv .proxyview-proxynameindicatordisabled {
|
||||
width: 5px;
|
||||
background: red;
|
||||
}
|
||||
|
||||
#jobsDiv jobsview-component .monaco-toolbar.carbon-taskbar,
|
||||
#operatorsDiv joboperatorsview-component .monaco-toolbar.carbon-taskbar,
|
||||
#alertsDiv jobalertsview-component .monaco-toolbar.carbon-taskbar,
|
||||
#proxiesDiv jobproxiesview-component .monaco-toolbar.carbon-taskbar {
|
||||
margin: 10px 0px 10px 0px;
|
||||
}
|
||||
@@ -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,7 +156,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div #jobsteps style="height: 100%">
|
||||
<div #jobsteps *ngIf="showSteps === true" style="flex: 1 1 auto; position: relative">
|
||||
<jobstepsview-component *ngIf="showSteps === true"></jobstepsview-component>
|
||||
</div>
|
||||
<h3 *ngIf="showSteps === false">No Steps Available</h3>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -64,7 +65,6 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
|
||||
private _agentJobInfo: sqlops.AgentJobInfo;
|
||||
private _noJobsAvailable: boolean = false;
|
||||
|
||||
private static readonly INITIAL_TREE_HEIGHT: number = 780;
|
||||
private static readonly HEADING_HEIGHT: number = 24;
|
||||
|
||||
constructor(
|
||||
@@ -77,7 +77,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 +142,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 +294,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 +304,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
.all-jobs {
|
||||
display: inline;
|
||||
font-size: 15px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.overview-container .overview-tab .resultsViewCollapsible {
|
||||
@@ -177,17 +176,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 +240,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 {
|
||||
@@ -257,14 +265,22 @@ jobhistory-component > .jobhistory-heading-container > .icon.in-progress {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
jobhistory-component > .agent-actionbar-container .monaco-action-bar > ul.actions-container {
|
||||
jobhistory-component > .agent-actionbar-container {
|
||||
border-top: 3px solid #f4f4f4;
|
||||
}
|
||||
|
||||
.vs-dark jobhistory-component > .agent-actionbar-container .monaco-action-bar > ul.actions-container {
|
||||
.vs-dark jobhistory-component > .agent-actionbar-container {
|
||||
border-top: 3px solid #444444;
|
||||
}
|
||||
|
||||
.hc-black jobhistory-component > .agent-actionbar-container .monaco-action-bar > ul.actions-container {
|
||||
.hc-black jobhistory-component > .agent-actionbar-container {
|
||||
border-top: 3px solid #2b56f2;
|
||||
}
|
||||
|
||||
jobhistory-component .step-table.prev-run-list .monaco-tree-wrapper .monaco-tree-row {
|
||||
width: 96%;
|
||||
}
|
||||
|
||||
jobhistory-component .agent-actionbar-container > .monaco-toolbar.carbon-taskbar {
|
||||
margin: 10px 0px 5px 0px;
|
||||
}
|
||||
@@ -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,13 @@
|
||||
}
|
||||
|
||||
jobstepsview-component {
|
||||
padding-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row {
|
||||
width: 99.2%;
|
||||
}
|
||||
@@ -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() {
|
||||
@@ -587,7 +591,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
|
||||
|
||||
private async curateJobHistory(jobs: sqlops.AgentJobInfo[], ownerUri: string) {
|
||||
const self = this;
|
||||
jobs.forEach(async (job) => {
|
||||
await Promise.all(jobs.map(async (job) => {
|
||||
await this._jobManagementService.getJobHistory(ownerUri, job.jobId, job.name).then(async(result) => {
|
||||
if (result) {
|
||||
self.jobSteps[job.jobId] = result.steps ? result.steps : [];
|
||||
@@ -618,32 +622,23 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private createJobChart(jobId: string, jobHistories: sqlops.AgentJobHistoryInfo[]): void {
|
||||
let chartHeights = this.getChartHeights(jobHistories);
|
||||
let runCharts = [];
|
||||
for (let i = 0; i < jobHistories.length; i++) {
|
||||
for (let i = 0; i < chartHeights.length; i++) {
|
||||
let runGraph = $(`table#${jobId}.jobprevruns > tbody > tr > td > div.bar${i}`);
|
||||
if (jobHistories && jobHistories.length > 0) {
|
||||
runGraph.css('height', chartHeights[i]);
|
||||
let bgColor = jobHistories[i].runStatus === 0 ? 'red' : 'green';
|
||||
runGraph.css('background', bgColor);
|
||||
runGraph.hover((e) => {
|
||||
let currentTarget = e.currentTarget;
|
||||
currentTarget.title = jobHistories[i].runDuration;
|
||||
});
|
||||
if (runGraph.get(0)) {
|
||||
runCharts.push(runGraph.get(0).outerHTML);
|
||||
}
|
||||
} else {
|
||||
runGraph.css('height', '5px');
|
||||
runGraph.css('background', 'red');
|
||||
runGraph.hover((e) => {
|
||||
let currentTarget = e.currentTarget;
|
||||
currentTarget.title = 'Job not run.';
|
||||
});
|
||||
runGraph.css('height', chartHeights[i]);
|
||||
let bgColor = jobHistories[i].runStatus === 0 ? 'red' : 'green';
|
||||
runGraph.css('background', bgColor);
|
||||
runGraph.hover((e) => {
|
||||
let currentTarget = e.currentTarget;
|
||||
currentTarget.title = jobHistories[i].runDuration;
|
||||
});
|
||||
if (runGraph.get(0)) {
|
||||
runCharts.push(runGraph.get(0).outerHTML);
|
||||
}
|
||||
}
|
||||
if (runCharts.length > 0) {
|
||||
@@ -654,7 +649,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
|
||||
// chart height normalization logic
|
||||
private getChartHeights(jobHistories: sqlops.AgentJobHistoryInfo[]): string[] {
|
||||
if (!jobHistories || jobHistories.length === 0) {
|
||||
return ['5px', '5px', '5px', '5px', '5px'];
|
||||
return [];
|
||||
}
|
||||
let maxDuration: number = 0;
|
||||
jobHistories.forEach(history => {
|
||||
@@ -933,19 +928,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;
|
||||
}
|
||||
|
||||
@@ -2,31 +2,32 @@
|
||||
<span *ngIf="hasStatus" class="card-status">
|
||||
<div class="status-content" [style.backgroundColor]="statusColor"></div>
|
||||
</span>
|
||||
|
||||
<ng-container *ngIf="isVerticalButton">
|
||||
<div class="card-vertical-button">
|
||||
<div *ngIf="iconPath" class="iconContainer">
|
||||
<div [class]="iconClass" [style.maxWidth]="iconWidth" [style.maxHeight]="iconHeight"></div>
|
||||
</div>
|
||||
<h4 class="card-label">{{label}}</h4>
|
||||
<span *ngIf="showRadioButton" class="selection-indicator-container">
|
||||
<div *ngIf="showAsSelected" class="selection-indicator"></div>
|
||||
</span>
|
||||
<ng-container *ngIf="isVerticalButton">
|
||||
<div class="card-vertical-button">
|
||||
<div *ngIf="iconPath" class="iconContainer">
|
||||
<div [class]="iconClass" [style.maxWidth]="iconWidth" [style.maxHeight]="iconHeight"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="isDetailsCard">
|
||||
<div class="card-content">
|
||||
<h4 class="card-label">{{label}}</h4>
|
||||
<p class="card-value">{{value}}</p>
|
||||
<span *ngIf="actions">
|
||||
<table class="model-table">
|
||||
<tr *ngFor="let action of actions">
|
||||
<td class="table-row">{{action.label}}</td>
|
||||
<td *ngIf="action.actionTitle" class="table-row">
|
||||
<a class="pointer prominent" (click)="onDidActionClick(action)">{{action.actionTitle}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h4 class="card-label">{{label}}</h4>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="isDetailsCard">
|
||||
<div class="card-content">
|
||||
<h4 class="card-label">{{label}}</h4>
|
||||
<p class="card-value">{{value}}</p>
|
||||
<span *ngIf="actions">
|
||||
<table class="model-table">
|
||||
<tr *ngFor="let action of actions">
|
||||
<td class="table-row">{{action.label}}</td>
|
||||
<td *ngIf="action.actionTitle" class="table-row">
|
||||
<a class="pointer prominent" (click)="onDidActionClick(action)">{{action.actionTitle}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -29,7 +29,7 @@ export default class CardComponent extends ComponentWithIconBase implements ICom
|
||||
|
||||
private backgroundColor: string;
|
||||
|
||||
constructor( @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
|
||||
constructor(@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
|
||||
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService
|
||||
) {
|
||||
@@ -130,6 +130,14 @@ export default class CardComponent extends ComponentWithIconBase implements ICom
|
||||
return this.cardType === 'VerticalButton';
|
||||
}
|
||||
|
||||
public get showRadioButton():boolean{
|
||||
return this.selectable && (this.selected || this._hasFocus)
|
||||
}
|
||||
|
||||
public get showAsSelected(): boolean {
|
||||
return this.selectable && this.selected;
|
||||
}
|
||||
|
||||
|
||||
public get actions(): ActionDescriptor[] {
|
||||
return this.getPropertyOrDefault<CardProperties, ActionDescriptor[]>((props) => props.actions, []);
|
||||
@@ -156,6 +164,7 @@ export default class CardComponent extends ComponentWithIconBase implements ICom
|
||||
|
||||
private updateTheme(theme: IColorTheme) {
|
||||
this.backgroundColor = theme.getColor(colors.editorBackground, true).toString();
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
private onDidActionClick(action: ActionDescriptor): void {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
.model-card {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
@@ -7,23 +6,18 @@
|
||||
margin: 15px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
box-shadow: rgba(120, 120, 120, 0.75) 0px 0px 6px;
|
||||
}
|
||||
|
||||
.model-card.selected {
|
||||
border-color: darkblue
|
||||
}
|
||||
|
||||
.vs-dark .monaco-workbench .model-card.selected,
|
||||
.hc-black .monaco-workbench .model-card.selected {
|
||||
border-color: darkblue
|
||||
border-color: rgb(0, 120, 215);
|
||||
box-shadow: rgba(0, 120, 215, 0.75) 0px 0px 6px;
|
||||
}
|
||||
|
||||
.model-card.unselected {
|
||||
border-color: rgb(214, 214, 214);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,21 +96,43 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.model-card .selection-indicator-container {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
overflow: hidden;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
border-width: 1px;
|
||||
border-color: rgb(0, 120, 215);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.model-card .selection-indicator {
|
||||
margin: 4px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: rgb(0, 120, 215);
|
||||
}
|
||||
|
||||
.model-card .model-table {
|
||||
border-spacing: 5px;
|
||||
}
|
||||
|
||||
.model-table .table-row {
|
||||
width: auto;
|
||||
clear: both;
|
||||
width: auto;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.model-table .table-cell {
|
||||
vertical-align: top;
|
||||
padding: 7px;
|
||||
vertical-align: top;
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.model-table a {
|
||||
cursor: pointer;
|
||||
text-decoration: underline
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
127
src/sql/parts/notebook/cellToggleMoreActions.ts
Normal file
127
src/sql/parts/notebook/cellToggleMoreActions.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ElementRef } from '@angular/core';
|
||||
|
||||
import { nb } from 'sqlops';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
|
||||
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { CellContext, CellActionBase } from 'sql/parts/notebook/cellViews/codeActions';
|
||||
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
||||
import { ToggleMoreWidgetAction } from 'sql/parts/dashboard/common/actions';
|
||||
import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts';
|
||||
import { CellModel } from 'sql/parts/notebook/models/cell';
|
||||
|
||||
export class CellToggleMoreActions {
|
||||
private _actions: Action[] = [];
|
||||
private _moreActions: ActionBar;
|
||||
constructor(
|
||||
@IInstantiationService private instantiationService: IInstantiationService) {
|
||||
this._actions.push(
|
||||
instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', 'Delete')),
|
||||
instantiationService.createInstance(AddCellFromContextAction,'codeBefore', localize('codeBefore', 'Insert Code before'), CellTypes.Code, false),
|
||||
instantiationService.createInstance(AddCellFromContextAction, 'codeAfter', localize('codeAfter', 'Insert Code after'), CellTypes.Code, true),
|
||||
instantiationService.createInstance(AddCellFromContextAction, 'markdownBefore', localize('markdownBefore', 'Insert Markdown before'), CellTypes.Markdown, false),
|
||||
instantiationService.createInstance(AddCellFromContextAction, 'markdownAfter', localize('markdownAfter', 'Insert Markdown after'), CellTypes.Markdown, true),
|
||||
instantiationService.createInstance(ClearCellOutputAction, 'clear', localize('clear', 'Clear output'))
|
||||
);
|
||||
}
|
||||
|
||||
public toggle(showIcon: boolean, elementRef: ElementRef, model: NotebookModel, cellModel: ICellModel) {
|
||||
let context = new CellContext(model,cellModel);
|
||||
let moreActionsElement = <HTMLElement>elementRef.nativeElement;
|
||||
if (showIcon) {
|
||||
if (moreActionsElement.childNodes.length > 0) {
|
||||
moreActionsElement.removeChild(moreActionsElement.childNodes[0]);
|
||||
}
|
||||
this._moreActions = new ActionBar(moreActionsElement, { orientation: ActionsOrientation.VERTICAL });
|
||||
this._moreActions.context = { target: moreActionsElement };
|
||||
this._moreActions.push(this.instantiationService.createInstance(ToggleMoreWidgetAction, this._actions, context), { icon: showIcon, label: false });
|
||||
}
|
||||
else if (moreActionsElement.childNodes.length > 0) {
|
||||
moreActionsElement.removeChild(moreActionsElement.childNodes[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AddCellFromContextAction extends CellActionBase {
|
||||
constructor(
|
||||
id: string, label: string, private cellType: CellType, private isAfter: boolean,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
runCellAction(context: CellContext): Promise<void> {
|
||||
try {
|
||||
let model = context.model;
|
||||
let index = model.cells.findIndex((cell) => cell.id === context.cell.id);
|
||||
if (index !== undefined && this.isAfter) {
|
||||
index += 1;
|
||||
}
|
||||
model.addCell(this.cellType, index);
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteCellAction extends CellActionBase {
|
||||
constructor(id: string, label: string,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
runCellAction(context: CellContext): Promise<void> {
|
||||
try {
|
||||
context.model.deleteCell(context.cell);
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export class ClearCellOutputAction extends CellActionBase {
|
||||
constructor(id: string, label: string,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
runCellAction(context: CellContext): Promise<void> {
|
||||
try {
|
||||
(context.model.activeCell as CellModel).clearOutputs();
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,6 +9,6 @@
|
||||
</div>
|
||||
<div #editor class="editor" style="flex: 1 1 auto; overflow: hidden;">
|
||||
</div>
|
||||
<div #moreactions class="toolbar" style="flex: 0 0 auto; display: flex; flex-flow:column; width: 20px; min-height: 20px; max-height: 20px; padding-top: 0px; orientation: portrait">
|
||||
<div #moreactions class="moreActions" style="flex: 0 0 auto; display: flex; flex-flow:column;width: 20px; min-height: 20px; max-height: 20px; padding-top: 0px; orientation: portrait">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,16 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import 'vs/css!./code';
|
||||
|
||||
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
|
||||
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
|
||||
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { AngularDisposable } from 'sql/base/common/lifecycle';
|
||||
import { QueryTextEditor } from 'sql/parts/modelComponents/queryTextEditor';
|
||||
import { CellToggleMoreActions } from 'sql/parts/notebook/cellToggleMoreActions';
|
||||
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { RunCellAction, CellContext } from 'sql/parts/notebook/cellViews/codeActions';
|
||||
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
||||
|
||||
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
@@ -19,20 +24,11 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { RunCellAction, DeleteCellAction, AddCellAction, CellContext } from 'sql/parts/notebook/cellViews/codeActions';
|
||||
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
||||
import { ToggleMoreWidgetAction } from 'sql/parts/dashboard/common/actions';
|
||||
import { CellTypes } from 'sql/parts/notebook/models/contracts';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
|
||||
export const CODE_SELECTOR: string = 'code-component';
|
||||
@@ -59,15 +55,14 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
}
|
||||
|
||||
protected _actionBar: Taskbar;
|
||||
protected _moreActions: ActionBar;
|
||||
private readonly _minimumHeight = 30;
|
||||
private _editor: QueryTextEditor;
|
||||
private _editorInput: UntitledEditorInput;
|
||||
private _editorModel: ITextModel;
|
||||
private _uri: string;
|
||||
private _model: NotebookModel;
|
||||
private _actions: Action[] = [];
|
||||
private _activeCellId: string;
|
||||
private _cellToggleMoreActions: CellToggleMoreActions;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
|
||||
@@ -81,13 +76,7 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
@Inject(INotificationService) private notificationService: INotificationService,
|
||||
) {
|
||||
super();
|
||||
this._actions.push(
|
||||
this._instantiationService.createInstance(AddCellAction, 'codeBefore', localize('codeBefore', 'Insert Code before'), CellTypes.Code, false),
|
||||
this._instantiationService.createInstance(AddCellAction, 'codeAfter', localize('codeAfter', 'Insert Code after'), CellTypes.Code, true),
|
||||
this._instantiationService.createInstance(AddCellAction, 'markdownBefore', localize('markdownBefore', 'Insert Markdown before'), CellTypes.Markdown, false),
|
||||
this._instantiationService.createInstance(AddCellAction, 'markdownAfter', localize('markdownAfter', 'Insert Markdown after'), CellTypes.Markdown, true),
|
||||
this._instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', 'Delete'))
|
||||
);
|
||||
this._cellToggleMoreActions = this._instantiationService.createInstance(CellToggleMoreActions);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -105,10 +94,10 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
if (propName === 'activeCellId') {
|
||||
let changedProp = changes[propName];
|
||||
if (this.cellModel.id === changedProp.currentValue) {
|
||||
this.toggleMoreActions(true);
|
||||
this._cellToggleMoreActions.toggle(true, this.moreActionsElementRef, this.model, this.cellModel);
|
||||
}
|
||||
else {
|
||||
this.toggleMoreActions(false);
|
||||
this._cellToggleMoreActions.toggle(false, this.moreActionsElementRef, this.model, this.cellModel);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -173,21 +162,6 @@ export class CodeComponent extends AngularDisposable implements OnInit, OnChange
|
||||
]);
|
||||
}
|
||||
|
||||
private toggleMoreActions(showIcon: boolean) {
|
||||
let context = new CellContext(this.model, this.cellModel);
|
||||
let moreActionsElement = <HTMLElement>this.moreActionsElementRef.nativeElement;
|
||||
if (showIcon) {
|
||||
if (moreActionsElement.childNodes.length > 0) {
|
||||
moreActionsElement.removeChild(moreActionsElement.childNodes[0]);
|
||||
}
|
||||
this._moreActions = new ActionBar(moreActionsElement, { orientation: ActionsOrientation.VERTICAL });
|
||||
this._moreActions.context = { target: moreActionsElement };
|
||||
this._moreActions.push(this._instantiationService.createInstance(ToggleMoreWidgetAction, this._actions, context), { icon: showIcon, label: false });
|
||||
}
|
||||
else if (moreActionsElement.childNodes.length > 0) {
|
||||
moreActionsElement.removeChild(moreActionsElement.childNodes[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private createUri(): URI {
|
||||
let uri = URI.from({ scheme: Schemas.untitled, path: `notebook-editor-${this.cellModel.id}` });
|
||||
|
||||
@@ -45,10 +45,6 @@ code-component .carbon-taskbar .icon {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
code-component .action-label.icon.toggle-more {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
code-component .carbon-taskbar.monaco-toolbar .monaco-action-bar.animated .actions-container
|
||||
{
|
||||
|
||||
@@ -40,7 +40,7 @@ export class CellContext {
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CellActionBase extends Action {
|
||||
export abstract class CellActionBase extends Action {
|
||||
|
||||
constructor(id: string, label: string, icon: string, protected notificationService: INotificationService) {
|
||||
super(id, label, icon);
|
||||
@@ -135,53 +135,3 @@ export class RunCellAction extends ToggleableAction {
|
||||
return clientSession.kernel;
|
||||
}
|
||||
}
|
||||
|
||||
export class AddCellAction extends CellActionBase {
|
||||
constructor(
|
||||
id: string, label: string, private cellType: CellType, private isAfter: boolean,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
runCellAction(context: CellContext): Promise<void> {
|
||||
try {
|
||||
let model = context.model;
|
||||
let index = model.cells.findIndex((cell) => cell.id === context.cell.id);
|
||||
if (index !== undefined && this.isAfter) {
|
||||
index += 1;
|
||||
}
|
||||
model.addCell(this.cellType, index);
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteCellAction extends CellActionBase {
|
||||
constructor(id: string, label: string,
|
||||
@INotificationService notificationService: INotificationService
|
||||
) {
|
||||
super(id, label, undefined, notificationService);
|
||||
}
|
||||
|
||||
runCellAction(context: CellContext): Promise<void> {
|
||||
try {
|
||||
context.model.deleteCell(context.cell);
|
||||
} catch (error) {
|
||||
let message = getErrorMessage(error);
|
||||
|
||||
this.notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: message
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,15 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
||||
<loading-spinner [loading]="isLoading"></loading-spinner>
|
||||
<div class="notebook-text" style="flex: 0 0 auto;">
|
||||
<code-component *ngIf="isEditMode" [cellModel]="cellModel" (onContentChanged)="handleContentChanged()" [model]="model" [activeCellId]="activeCellId" [hideVerticalToolbar]=true>
|
||||
</code-component>
|
||||
</div>
|
||||
<div #preview style="flex: 0 0 auto;" (dblclick)="toggleEditMode()">
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: row">
|
||||
<div #preview class ="notebook-preview" style="flex: 1 1 auto; user-select: initial;" (dblclick)="toggleEditMode()">
|
||||
</div>
|
||||
<div #moreactions class="moreActions" style="flex: 0 0 auto; display: flex; flex-flow:column;width: 20px; min-height: 20px; max-height: 20px; padding-top: 0px; orientation: portrait">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,16 +6,21 @@ import 'vs/css!./textCell';
|
||||
|
||||
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, OnChanges, SimpleChange } from '@angular/core';
|
||||
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { CellView } from 'sql/parts/notebook/cellViews/interfaces';
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
import * as themeColors from 'vs/workbench/common/theme';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { CellView } from 'sql/parts/notebook/cellViews/interfaces';
|
||||
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { ISanitizer, defaultSanitizer } from 'sql/parts/notebook/outputs/sanitizer';
|
||||
import { localize } from 'vs/nls';
|
||||
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
||||
import { CellToggleMoreActions } from 'sql/parts/notebook/cellToggleMoreActions';
|
||||
|
||||
export const TEXT_SELECTOR: string = 'text-cell-component';
|
||||
|
||||
@@ -25,6 +30,7 @@ export const TEXT_SELECTOR: string = 'text-cell-component';
|
||||
})
|
||||
export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
@ViewChild('preview', { read: ElementRef }) private output: ElementRef;
|
||||
@ViewChild('moreactions', { read: ElementRef }) private moreActionsElementRef: ElementRef;
|
||||
@Input() cellModel: ICellModel;
|
||||
|
||||
@Input() set model(value: NotebookModel) {
|
||||
@@ -38,18 +44,25 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
private _content: string;
|
||||
private isEditMode: boolean;
|
||||
private _sanitizer: ISanitizer;
|
||||
private _previewCssApplied: boolean = false;
|
||||
private _model: NotebookModel;
|
||||
private _activeCellId: string;
|
||||
private readonly _onDidClickLink = this._register(new Emitter<URI>());
|
||||
public readonly onDidClickLink = this._onDidClickLink.event;
|
||||
protected isLoading: boolean;
|
||||
private _cellToggleMoreActions: CellToggleMoreActions;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||
@Inject(ICommandService) private _commandService: ICommandService
|
||||
@Inject(ICommandService) private _commandService: ICommandService,
|
||||
@Inject(IOpenerService) private readonly openerService: IOpenerService,
|
||||
) {
|
||||
super();
|
||||
this.isEditMode = false;
|
||||
this.isLoading = true;
|
||||
this._cellToggleMoreActions = this._instantiationService.createInstance(CellToggleMoreActions);
|
||||
}
|
||||
|
||||
//Gets sanitizer from ISanitizer interface
|
||||
@@ -68,8 +81,14 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
return this._activeCellId;
|
||||
}
|
||||
|
||||
private setLoading(isLoading: boolean): void {
|
||||
this.isLoading = isLoading;
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.updatePreview();
|
||||
this.setLoading(false);
|
||||
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
||||
this.updateTheme(this.themeService.getColorTheme());
|
||||
this.cellModel.onOutputsChanged(e => {
|
||||
@@ -82,9 +101,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 +134,7 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Todo: implement layout
|
||||
public layout() {
|
||||
@@ -126,32 +143,24 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
private updateTheme(theme: IColorTheme): void {
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||
|
||||
let moreActionsEl = <HTMLElement>this.moreActionsElementRef.nativeElement;
|
||||
moreActionsEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||
}
|
||||
|
||||
public handleContentChanged(): void {
|
||||
if (!this._previewCssApplied) {
|
||||
this.updatePreviewCssClass();
|
||||
}
|
||||
this.updatePreview();
|
||||
}
|
||||
|
||||
public toggleEditMode(editMode?: boolean): void {
|
||||
this.isEditMode = editMode !== undefined? editMode : !this.isEditMode;
|
||||
this.updatePreviewCssClass();
|
||||
if (!this.isEditMode && this.cellModel.id === this._activeCellId) {
|
||||
this._cellToggleMoreActions.toggle(true, this.moreActionsElementRef, this.model, this.cellModel);
|
||||
}
|
||||
else {
|
||||
this._cellToggleMoreActions.toggle(false, this.moreActionsElementRef, this.model, this.cellModel);
|
||||
}
|
||||
this.updatePreview();
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
// Updates the css class to preview 'div' based on edit mode
|
||||
private updatePreviewCssClass() {
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
if (this.isEditMode && this.cellModel.source) {
|
||||
outputElement.className = 'notebook-preview';
|
||||
this._previewCssApplied = true;
|
||||
}
|
||||
else {
|
||||
outputElement.className = '';
|
||||
this._previewCssApplied = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,6 @@ text-cell-component .notebook-preview {
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
user-select: initial;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
@@ -35,18 +35,18 @@ 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
|
||||
if (cellData) {
|
||||
// Read in contents if available
|
||||
this.fromJSON(cellData);
|
||||
} else {
|
||||
this._cellType = CellTypes.Code;
|
||||
this._source = '';
|
||||
}
|
||||
this._isEditMode = this._cellType !== CellTypes.Markdown;
|
||||
this.setDefaultLanguage();
|
||||
this.ensureDefaultLanguage();
|
||||
if (_options && _options.isTrusted) {
|
||||
this._isTrusted = true;
|
||||
} else {
|
||||
@@ -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,16 +275,16 @@ 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;
|
||||
}
|
||||
this._cellType = cell.cell_type;
|
||||
this._source = Array.isArray(cell.source) ? cell.source.join('') : cell.source;
|
||||
this._language = (cell.metadata && cell.metadata.language) ? cell.metadata.language : 'python';
|
||||
this.setLanguageFromContents(cell);
|
||||
if (cell.outputs) {
|
||||
for (let output of cell.outputs) {
|
||||
// For now, we're assuming it's OK to save these as-is with no modification
|
||||
@@ -293,6 +293,15 @@ export class CellModel implements ICellModel {
|
||||
}
|
||||
}
|
||||
|
||||
private setLanguageFromContents(cell: nb.ICellContents): void {
|
||||
if (cell.cell_type === CellTypes.Markdown) {
|
||||
this._language = 'markdown';
|
||||
} else if (cell.metadata && cell.metadata.language) {
|
||||
this._language = cell.metadata.language;
|
||||
}
|
||||
// else skip, we set default language anyhow
|
||||
}
|
||||
|
||||
private addOutput(output: nb.ICellOutput) {
|
||||
this._normalize(output);
|
||||
this._outputs.push(output);
|
||||
@@ -327,8 +336,32 @@ export class CellModel implements ICellModel {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private setDefaultLanguage(): void {
|
||||
this._language = 'python';
|
||||
/**
|
||||
* Ensures there is a default language set, if none was already defined.
|
||||
* Will read information from the overall Notebook (passed as options to the model), or
|
||||
* if all else fails default back to python.
|
||||
*
|
||||
*/
|
||||
private ensureDefaultLanguage(): void {
|
||||
// See if language is already set / is known based on cell type
|
||||
if (this.hasLanguage()) {
|
||||
return;
|
||||
}
|
||||
if (this._cellType === CellTypes.Markdown) {
|
||||
this._language = 'markdown';
|
||||
return;
|
||||
}
|
||||
|
||||
// try set it based on overall Notebook language
|
||||
this.trySetLanguageFromLangInfo();
|
||||
|
||||
// fallback to python
|
||||
if (!this._language) {
|
||||
this._language = 'python';
|
||||
}
|
||||
}
|
||||
|
||||
private trySetLanguageFromLangInfo() {
|
||||
// In languageInfo, set the language to the "name" property
|
||||
// If the "name" property isn't defined, check the "mimeType" property
|
||||
// Otherwise, default to python as the language
|
||||
@@ -338,16 +371,25 @@ export class CellModel implements ICellModel {
|
||||
// check the LanguageMapping to determine if a mapping is necessary (example 'pyspark' -> 'python')
|
||||
if (CellModel.LanguageMapping[languageInfo.name]) {
|
||||
this._language = CellModel.LanguageMapping[languageInfo.name];
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this._language = languageInfo.name;
|
||||
}
|
||||
} else if (languageInfo.mimetype) {
|
||||
}
|
||||
else if (languageInfo.mimetype) {
|
||||
this._language = languageInfo.mimetype;
|
||||
}
|
||||
}
|
||||
let mimeTypePrefix = 'x-';
|
||||
if (this._language.includes(mimeTypePrefix)) {
|
||||
this._language = this._language.replace(mimeTypePrefix, '');
|
||||
|
||||
if (this._language) {
|
||||
let mimeTypePrefix = 'x-';
|
||||
if (this._language.includes(mimeTypePrefix)) {
|
||||
this._language = this._language.replace(mimeTypePrefix, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private hasLanguage(): boolean {
|
||||
return !!this._language;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -287,6 +288,12 @@ export interface INotebookModel {
|
||||
*/
|
||||
readonly contexts: IDefaultConnection | undefined;
|
||||
|
||||
/**
|
||||
* Event fired on first initialization of the cells and
|
||||
* on subsequent change events
|
||||
*/
|
||||
readonly contentChanged: Event<NotebookContentChange>;
|
||||
|
||||
/**
|
||||
* The trusted mode of the Notebook
|
||||
*/
|
||||
@@ -328,6 +335,37 @@ 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 NotebookContentChange {
|
||||
/**
|
||||
* The type of change that occurred
|
||||
*/
|
||||
changeType: NotebookChangeType;
|
||||
/**
|
||||
* Optional cells that were changed
|
||||
*/
|
||||
cells?: ICellModel | ICellModel[];
|
||||
/**
|
||||
* Optional index of the change, indicating the cell at which an insert or
|
||||
* delete occurred
|
||||
*/
|
||||
cellIndex?: number;
|
||||
/**
|
||||
* Optional value indicating if the notebook is in a dirty or clean state after this change
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof NotebookContentChange
|
||||
*/
|
||||
isDirty?: boolean;
|
||||
}
|
||||
|
||||
export interface ICellModelOptions {
|
||||
@@ -348,7 +386,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 +395,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
import { CellModel } from './cell';
|
||||
import { IClientSession, INotebookModel, IDefaultConnection, INotebookModelOptions, ICellModel, notebookConstants } from './modelInterfaces';
|
||||
import { IClientSession, INotebookModel, IDefaultConnection, INotebookModelOptions, ICellModel, notebookConstants, NotebookContentChange } from './modelInterfaces';
|
||||
import { NotebookChangeType, CellTypes, CellType } from 'sql/parts/notebook/models/contracts';
|
||||
import { nbversion } from '../notebookConstants';
|
||||
import * as notebookUtils from '../notebookUtils';
|
||||
@@ -22,6 +22,8 @@ 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 URI from 'vs/base/common/uri';
|
||||
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
|
||||
/*
|
||||
* Used to control whether a message in a dialog/wizard is displayed as an error,
|
||||
@@ -37,28 +39,6 @@ export class ErrorInfo {
|
||||
constructor(public readonly message: string, public readonly severity: MessageLevel) {
|
||||
}
|
||||
}
|
||||
export interface NotebookContentChange {
|
||||
/**
|
||||
* What was the change that occurred?
|
||||
*/
|
||||
changeType: NotebookChangeType;
|
||||
/**
|
||||
* Optional cells that were changed
|
||||
*/
|
||||
cells?: ICellModel | ICellModel[];
|
||||
/**
|
||||
* Optional index of the change, indicating the cell at which an insert or
|
||||
* delete occurred
|
||||
*/
|
||||
cellIndex?: number;
|
||||
/**
|
||||
* Optional value indicating if the notebook is in a dirty or clean state after this change
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof NotebookContentChange
|
||||
*/
|
||||
isDirty?: boolean;
|
||||
}
|
||||
|
||||
export class NotebookModel extends Disposable implements INotebookModel {
|
||||
private _contextsChangedEmitter = new Emitter<void>();
|
||||
@@ -96,6 +76,13 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
||||
return this.notebookOptions.notebookManager;
|
||||
}
|
||||
|
||||
public get notebookUri() : URI {
|
||||
return this.notebookOptions.notebookUri;
|
||||
}
|
||||
public set notebookUri(value : URI) {
|
||||
this.notebookOptions.notebookUri = value;
|
||||
}
|
||||
|
||||
public get hasServerManager(): boolean {
|
||||
// If the service has a server manager, then we can show the start button
|
||||
return !!this.notebookManager.serverManager;
|
||||
@@ -237,7 +224,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 +250,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 +287,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 +400,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 +409,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 +501,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,12 +140,19 @@ 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) {
|
||||
// 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;
|
||||
defaultKernel = !foundSavedKernelInSpecs ? specs.kernels.find((spec) => spec.name === notebookConstants.defaultSparkKernel) : foundSavedKernelInSpecs;
|
||||
} else {
|
||||
// Handle kernels
|
||||
if (savedKernelInfo && savedKernelInfo.name.toLowerCase().indexOf('spark') > -1) {
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
-->
|
||||
<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 #toolbar class="editor-toolbar actionbar-container" style="flex: 0 0 auto; display: flex; flex-flow: row; width: 100%; align-items: center;">
|
||||
</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">
|
||||
|
||||
@@ -5,41 +5,50 @@
|
||||
|
||||
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';
|
||||
import { INotificationService, INotification } from 'vs/platform/notification/common/notification';
|
||||
import { INotificationService, INotification, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
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 { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { NotebookModel, NotebookContentChange } from 'sql/parts/notebook/models/notebookModel';
|
||||
import { ModelFactory } from 'sql/parts/notebook/models/modelFactory';
|
||||
import * as notebookUtils from './notebookUtils';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { KernelsDropdown, AttachToDropdown, AddCellAction, TrustedAction, SaveNotebookAction } from 'sql/parts/notebook/notebookActions';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
|
||||
import { MenuId, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions';
|
||||
import { IAction, Action, IActionItem } from 'vs/base/common/actions';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { fillInActions, LabeledMenuItemActionItem } from 'vs/platform/actions/browser/menuItemActionItem';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { IHistoryService } from 'vs/workbench/services/history/common/history';
|
||||
import * as paths from 'vs/base/common/paths';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
import { VIEWLET_ID, IExtensionsViewlet } from 'vs/workbench/parts/extensions/common/extensions';
|
||||
|
||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||
import { AngularDisposable } from 'sql/base/common/lifecycle';
|
||||
import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts';
|
||||
import { ICellModel, IModelFactory, notebookConstants, INotebookModel, NotebookContentChange } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
import { IConnectionManagementService, IConnectionDialogService } from 'sql/parts/connection/common/connectionManagement';
|
||||
import { INotebookService, INotebookParams, INotebookManager, INotebookEditor, DEFAULT_NOTEBOOK_FILETYPE, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService';
|
||||
import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService';
|
||||
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
||||
import { ModelFactory } from 'sql/parts/notebook/models/modelFactory';
|
||||
import * as notebookUtils from 'sql/parts/notebook/notebookUtils';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { KernelsDropdown, AttachToDropdown, AddCellAction, TrustedAction, SaveNotebookAction } from 'sql/parts/notebook/notebookActions';
|
||||
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';
|
||||
import { IResourceInput } from 'vs/platform/editor/common/editor';
|
||||
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||
import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService';
|
||||
|
||||
export const NOTEBOOK_SELECTOR: string = 'notebook-component';
|
||||
|
||||
@@ -48,7 +57,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,14 +82,19 @@ 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,
|
||||
@Inject(IConnectionDialogService) private connectionDialogService: IConnectionDialogService,
|
||||
@Inject(IContextKeyService) private contextKeyService: IContextKeyService,
|
||||
@Inject(IMenuService) private menuService: IMenuService,
|
||||
@Inject(IKeybindingService) private keybindingService: IKeybindingService
|
||||
@Inject(IKeybindingService) private keybindingService: IKeybindingService,
|
||||
@Inject(IHistoryService) private historyService: IHistoryService,
|
||||
@Inject(IWindowService) private windowService: IWindowService,
|
||||
@Inject(IViewletService) private viewletService: IViewletService,
|
||||
@Inject(IUntitledEditorService) private untitledEditorService: IUntitledEditorService,
|
||||
@Inject(IEditorGroupsService) private editorGroupService: IEditorGroupsService
|
||||
) {
|
||||
super();
|
||||
this.updateProfile();
|
||||
@@ -108,10 +122,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;
|
||||
}
|
||||
@@ -124,7 +145,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
|
||||
return this._modelRegisteredDeferred.promise;
|
||||
}
|
||||
|
||||
protected get cells(): ReadonlyArray<ICellModel> {
|
||||
public get cells(): ICellModel[] {
|
||||
return this._model ? this._model.cells : [];
|
||||
}
|
||||
|
||||
@@ -133,7 +154,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 +170,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 +235,17 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
|
||||
}
|
||||
|
||||
private async loadModel(): Promise<void> {
|
||||
this.notebookManager = await this.notebookService.getOrCreateNotebookManager(this.notebookParams.providerId, this.notebookParams.notebookUri);
|
||||
await this.awaitNonDefaultProvider();
|
||||
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);
|
||||
@@ -220,6 +255,34 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
|
||||
this._changeRef.detectChanges();
|
||||
}
|
||||
|
||||
private async awaitNonDefaultProvider(): Promise<void> {
|
||||
// Wait on registration for now. Long-term would be good to cache and refresh
|
||||
await this.notebookService.registrationComplete;
|
||||
// Refresh the provider if we had been using default
|
||||
if (DEFAULT_NOTEBOOK_PROVIDER === this._notebookParams.providerId) {
|
||||
this._notebookParams.providerId = notebookUtils.getProviderForFileName(this._notebookParams.notebookUri.fsPath, this.notebookService);
|
||||
}
|
||||
if (DEFAULT_NOTEBOOK_PROVIDER === this._notebookParams.providerId) {
|
||||
// If it's still the default, warn them they should install an extension
|
||||
this.notificationService.prompt(Severity.Warning,
|
||||
localize('noKernelInstalled', 'Please install the SQL Server 2019 extension to run cells'),
|
||||
[{
|
||||
label: localize('installSql2019Extension', 'Install Extension'),
|
||||
run: () => this.openExtensionGallery()
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
private async openExtensionGallery(): Promise<void> {
|
||||
try {
|
||||
let viewlet = await this.viewletService.openViewlet(VIEWLET_ID, true) as IExtensionsViewlet;
|
||||
viewlet.search('sql-vnext');
|
||||
viewlet.focus();
|
||||
} catch (error) {
|
||||
this.notificationService.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Updates toolbar components
|
||||
private updateToolbarComponents(isTrusted: boolean)
|
||||
{
|
||||
@@ -231,10 +294,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);
|
||||
@@ -311,9 +374,83 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
// Gets file path from recent workspace in local
|
||||
private getLastActiveFilePath(untitledResource: URI): string {
|
||||
let fileName = untitledResource.path + '.' + DEFAULT_NOTEBOOK_FILETYPE.toLocaleLowerCase();
|
||||
|
||||
let lastActiveFile = this.historyService.getLastActiveFile();
|
||||
if (lastActiveFile) {
|
||||
return URI.file(paths.join(paths.dirname(lastActiveFile.fsPath), fileName)).fsPath;
|
||||
}
|
||||
|
||||
let lastActiveFolder = this.historyService.getLastActiveWorkspaceRoot('file');
|
||||
if (lastActiveFolder) {
|
||||
return URI.file(paths.join(lastActiveFolder.fsPath, fileName)).fsPath;
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
||||
promptForPath(defaultPath: string): TPromise<string> {
|
||||
return this.windowService.showSaveDialog({
|
||||
defaultPath: defaultPath,
|
||||
filters: [{ name: localize('notebookFile', 'Notebook'), extensions: ['ipynb']}]
|
||||
});
|
||||
}
|
||||
|
||||
// Entry point to save notebook
|
||||
public async save(): Promise<boolean> {
|
||||
let self = this;
|
||||
let notebookUri = this.notebookParams.notebookUri;
|
||||
if (notebookUri.scheme === Schemas.untitled) {
|
||||
let dialogPath = this.getLastActiveFilePath(notebookUri);
|
||||
return this.promptForPath(dialogPath).then(path => {
|
||||
if (path) {
|
||||
let target = URI.file(path);
|
||||
let resource = self._model.notebookUri;
|
||||
self._model.notebookUri = target;
|
||||
this.saveNotebook().then(result => {
|
||||
if(result)
|
||||
{
|
||||
return this.replaceUntitledNotebookEditor(resource, target);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
return false; // User clicks cancel
|
||||
});
|
||||
}
|
||||
else {
|
||||
return await this.saveNotebook();
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces untitled notebook editor with the saved file name
|
||||
private async replaceUntitledNotebookEditor(resource: URI, target: URI): Promise<boolean> {
|
||||
let encodingOfSource = this.untitledEditorService.getEncoding(resource);
|
||||
const replacement: IResourceInput = {
|
||||
resource: target,
|
||||
encoding: encodingOfSource,
|
||||
options: {
|
||||
pinned: true
|
||||
}
|
||||
};
|
||||
|
||||
return TPromise.join(this.editorGroupService.groups.map(g =>
|
||||
this.editorService.replaceEditors([{
|
||||
editor: { resource },
|
||||
replacement
|
||||
}], g))).then(() => {
|
||||
this.notebookService.renameNotebookEditor(resource, target, this);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private async saveNotebook(): 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 +459,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 +473,36 @@ 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();
|
||||
}
|
||||
|
||||
public get modelReady(): Promise<INotebookModel> {
|
||||
return this._modelReadyDeferred.promise;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.editorService.activeEditor === this.notebookParams.input;
|
||||
}
|
||||
|
||||
isVisible(): boolean {
|
||||
let notebookEditor = this.notebookParams.input;
|
||||
return this.editorService.visibleEditors.some(e => e === notebookEditor);
|
||||
}
|
||||
|
||||
isDirty(): boolean {
|
||||
return this.notebookParams.input.isDirty();
|
||||
}
|
||||
|
||||
executeEdits(edits: ISingleNotebookEditOperation[]): boolean {
|
||||
if (!edits || edits.length === 0) {
|
||||
return false;
|
||||
}
|
||||
this._model.pushEditOperations(edits);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: 100px;
|
||||
}
|
||||
|
||||
.notebookEditor .notebook-button.icon-add{
|
||||
background-image: url("./media/light/add.svg");
|
||||
}
|
||||
@@ -70,4 +74,9 @@
|
||||
.vs-dark .notebookEditor .notebook-button.icon-save,
|
||||
.hc-black .notebookEditor .notebook-button.icon-save{
|
||||
background-image: url("./media/dark/save_inverse.svg");
|
||||
}
|
||||
|
||||
.moreActions .action-label.icon.toggle-more {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
@@ -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: ');
|
||||
@@ -25,7 +26,6 @@ const attachToLabel: string = localize('AttachTo', 'Attach to: ');
|
||||
const msgLoadingContexts = localize('loadingContexts', 'Loading contexts...');
|
||||
const msgAddNewConnection = localize('addNewConnection', 'Add new connection');
|
||||
const msgSelectConnection = localize('selectConnection', 'Select connection');
|
||||
const msgConnectionNotApplicable = localize('connectionNotSupported', 'n/a');
|
||||
const msgLocalHost = localize('localhost', 'Localhost');
|
||||
|
||||
// Action to add a cell to notebook based on cell type(code/markdown).
|
||||
@@ -64,8 +64,6 @@ export class SaveNotebookAction extends Action {
|
||||
let saved = await context.save();
|
||||
if (saved) {
|
||||
this._notificationService.notify({ severity: Severity.Info, message: SaveNotebookAction.notebookSavedMsg, actions });
|
||||
} else {
|
||||
this._notificationService.error(SaveNotebookAction.notebookFailedSaveMsg);
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
@@ -238,7 +236,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 +312,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,28 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
public matches(otherInput: any): boolean {
|
||||
if (super.matches(otherInput) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (otherInput instanceof NotebookInput) {
|
||||
const otherNotebookEditorInput = <NotebookInput>otherInput;
|
||||
|
||||
// Compare by resource
|
||||
return otherNotebookEditorInput.notebookUri.toString() === this.notebookUri.toString();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -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,13 @@
|
||||
|
||||
'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 { DEFAULT_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_FILETYPE, INotebookService } from 'sql/services/notebook/notebookService';
|
||||
|
||||
|
||||
/**
|
||||
@@ -36,3 +38,22 @@ export async function mkDir(dirPath: string, outputChannel?: IOutputChannel): Pr
|
||||
await pfs.mkdirp(dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
export function getProviderForFileName(fileName: string, notebookService: INotebookService): string {
|
||||
let fileExt = path.extname(fileName);
|
||||
let provider: string;
|
||||
// First try to get provider for actual file type
|
||||
if (fileExt && fileExt.startsWith('.')) {
|
||||
fileExt = fileExt.slice(1,fileExt.length);
|
||||
provider = notebookService.getProviderForFileType(fileExt);
|
||||
}
|
||||
// Fallback to provider for default file type (assume this is a global handler)
|
||||
if (!provider) {
|
||||
provider = notebookService.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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { Sash, IHorizontalSashLayoutProvider, ISashEvent, Orientation } from 'vs/base/browser/ui/sash/sash';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser';
|
||||
import { FIND_IDS, MATCHES_LIMIT, CONTEXT_FIND_INPUT_FOCUSED } from 'vs/editor/contrib/find/findModel';
|
||||
import { FIND_IDS, CONTEXT_FIND_INPUT_FOCUSED } from 'vs/editor/contrib/find/findModel';
|
||||
import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/findState';
|
||||
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ITheme, registerThemingParticipant, IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
@@ -36,7 +36,7 @@ const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find");
|
||||
const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', "Previous match");
|
||||
const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next match");
|
||||
const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close");
|
||||
const NLS_MATCHES_COUNT_LIMIT_TITLE = nls.localize('title.matchesCountLimit', "Only the first 999 results are highlighted, but all find operations work on the entire text.");
|
||||
const NLS_MATCHES_COUNT_LIMIT_TITLE = nls.localize('title.matchesCountLimit', "Your search returned a large number of results, only the first 999 matches will be highlighted.");
|
||||
const NLS_MATCHES_LOCATION = nls.localize('label.matchesLocation', "{0} of {1}");
|
||||
const NLS_NO_RESULTS = nls.localize('label.noResults', "No Results");
|
||||
|
||||
@@ -46,6 +46,8 @@ const FIND_INPUT_AREA_WIDTH = PART_WIDTH - 54;
|
||||
|
||||
let MAX_MATCHES_COUNT_WIDTH = 69;
|
||||
|
||||
export const PROFILER_MAX_MATCHES = 999;
|
||||
|
||||
export const ACTION_IDS = {
|
||||
FIND_NEXT: 'findNext',
|
||||
FIND_PREVIOUS: 'findPrev'
|
||||
@@ -86,6 +88,8 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas
|
||||
|
||||
private _resizeSash: Sash;
|
||||
|
||||
private searchTimeoutHandle: number;
|
||||
|
||||
constructor(
|
||||
tableController: ITableController,
|
||||
state: FindReplaceState,
|
||||
@@ -213,7 +217,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas
|
||||
|
||||
private _updateMatchesCount(): void {
|
||||
this._matchesCount.style.minWidth = MAX_MATCHES_COUNT_WIDTH + 'px';
|
||||
if (this._state.matchesCount >= MATCHES_LIMIT) {
|
||||
if (this._state.matchesCount >= PROFILER_MAX_MATCHES) {
|
||||
this._matchesCount.title = NLS_MATCHES_COUNT_LIMIT_TITLE;
|
||||
} else {
|
||||
this._matchesCount.title = '';
|
||||
@@ -227,8 +231,8 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas
|
||||
let label: string;
|
||||
if (this._state.matchesCount > 0) {
|
||||
let matchesCount: string = String(this._state.matchesCount);
|
||||
if (this._state.matchesCount >= MATCHES_LIMIT) {
|
||||
matchesCount += '+';
|
||||
if (this._state.matchesCount >= PROFILER_MAX_MATCHES) {
|
||||
matchesCount = PROFILER_MAX_MATCHES + '+';
|
||||
}
|
||||
let matchesPosition: string = String(this._state.matchesPosition);
|
||||
if (matchesPosition === '0') {
|
||||
@@ -308,8 +312,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();
|
||||
}
|
||||
|
||||
@@ -403,7 +405,14 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas
|
||||
this._findInput.setWholeWords(!!this._state.wholeWord);
|
||||
this._register(this._findInput.onKeyDown((e) => this._onFindInputKeyDown(e)));
|
||||
this._register(this._findInput.onInput(() => {
|
||||
this._state.change({ searchString: this._findInput.getValue() }, true);
|
||||
let self = this;
|
||||
if (self.searchTimeoutHandle) {
|
||||
clearTimeout(self.searchTimeoutHandle);
|
||||
}
|
||||
|
||||
this.searchTimeoutHandle = setTimeout(function () {
|
||||
self._state.change({ searchString: self._findInput.getValue() }, true);
|
||||
}, 300);
|
||||
}));
|
||||
this._register(this._findInput.onDidOptionChange(() => {
|
||||
this._state.change({
|
||||
|
||||
@@ -27,6 +27,7 @@ import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Dimension } from 'vs/base/browser/dom';
|
||||
import { textFormatter } from 'sql/parts/grid/services/sharedServices';
|
||||
import { PROFILER_MAX_MATCHES } from 'sql/parts/profiler/editor/controller/profilerFindWidget';
|
||||
|
||||
export interface ProfilerTableViewState {
|
||||
scrollTop: number;
|
||||
@@ -214,10 +215,11 @@ export class ProfilerTableEditor extends BaseEditor implements IProfilerControll
|
||||
if (e.searchString) {
|
||||
if (this._input && this._input.data) {
|
||||
if (this._findState.searchString) {
|
||||
this._input.data.find(this._findState.searchString).then(p => {
|
||||
this._input.data.find(this._findState.searchString, PROFILER_MAX_MATCHES).then(p => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +305,11 @@ let registryProperties = {
|
||||
'description': localize('sql.saveAsCsv.encoding', '[Optional] File encoding used when saving results as CSV'),
|
||||
'default': 'utf-8'
|
||||
},
|
||||
'sql.results.streaming': {
|
||||
'type': 'boolean',
|
||||
'description': localize('sql.results.streaming', 'Enable results streaming; contains few minor visual issues'),
|
||||
'default': false
|
||||
},
|
||||
'sql.copyIncludeHeaders': {
|
||||
'type': 'boolean',
|
||||
'description': localize('sql.copyIncludeHeaders', '[Optional] Configuration options for copying results from the Results View'),
|
||||
|
||||
@@ -88,7 +88,8 @@ export class GridTableState extends Disposable {
|
||||
private _canBeMaximized: boolean;
|
||||
|
||||
/* The top row of the current scroll */
|
||||
public scrollPosition = 0;
|
||||
public scrollPositionY = 0;
|
||||
public scrollPositionX = 0;
|
||||
public selection: Slick.Range[];
|
||||
public activeCell: Slick.Cell;
|
||||
|
||||
@@ -145,7 +146,7 @@ export class GridPanel extends ViewletPanel {
|
||||
super(options, keybindingService, contextMenuService, configurationService);
|
||||
this.splitView = new ScrollableSplitView(this.container, { enableResizing: false, verticalScrollbarVisibility: ScrollbarVisibility.Visible });
|
||||
this.splitView.onScroll(e => {
|
||||
if (this.state) {
|
||||
if (this.state && this.splitView.length !== 0) {
|
||||
this.state.scrollPosition = e;
|
||||
}
|
||||
});
|
||||
@@ -185,62 +186,97 @@ export class GridPanel extends ViewletPanel {
|
||||
}
|
||||
this.reset();
|
||||
}));
|
||||
this.addResultSet(this.runner.batchSets.reduce<sqlops.ResultSetSummary[]>((p, e) => {
|
||||
if (this.configurationService.getValue<boolean>('sql.results.streaming')) {
|
||||
p = p.concat(e.resultSetSummaries);
|
||||
} else {
|
||||
p = p.concat(e.resultSetSummaries.filter(c => c.complete));
|
||||
}
|
||||
return p;
|
||||
}, []));
|
||||
this.maximumBodySize = this.tables.reduce((p, c) => {
|
||||
return p + c.maximumSize;
|
||||
}, 0);
|
||||
|
||||
if (this.state && this.state.scrollPosition) {
|
||||
this.splitView.setScrollPosition(this.state.scrollPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private onResultSet(resultSet: sqlops.ResultSetSummary | sqlops.ResultSetSummary[]) {
|
||||
this.addResultSet(resultSet);
|
||||
|
||||
this.tables.map(t => {
|
||||
t.state.canBeMaximized = this.tables.length > 1;
|
||||
});
|
||||
|
||||
this.maximumBodySize = this.tables.reduce((p, c) => {
|
||||
return p + c.maximumSize;
|
||||
}, 0);
|
||||
|
||||
if (this.state && this.state.scrollPosition) {
|
||||
this.splitView.setScrollPosition(this.state.scrollPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private updateResultSet(resultSet: sqlops.ResultSetSummary | sqlops.ResultSetSummary[]) {
|
||||
|
||||
let resultsToUpdate: sqlops.ResultSetSummary[];
|
||||
if (!Array.isArray(resultSet)) {
|
||||
resultsToUpdate = [resultSet];
|
||||
} else {
|
||||
resultsToUpdate = resultSet;
|
||||
}
|
||||
|
||||
for (let set of resultsToUpdate) {
|
||||
let table = this.tables.find(t => t.resultSet.batchId === set.batchId && t.resultSet.id === set.id);
|
||||
if (table) {
|
||||
table.updateResult(set);
|
||||
} else {
|
||||
warn('Got result set update request for non-existant table');
|
||||
}
|
||||
}
|
||||
|
||||
this.maximumBodySize = this.tables.reduce((p, c) => {
|
||||
return p + c.maximumSize;
|
||||
}, 0);
|
||||
|
||||
if (this.state && this.state.scrollPosition) {
|
||||
this.splitView.setScrollPosition(this.state.scrollPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private addResultSet(resultSet: sqlops.ResultSetSummary | sqlops.ResultSetSummary[]) {
|
||||
let resultsToAdd: sqlops.ResultSetSummary[];
|
||||
if (!Array.isArray(resultSet)) {
|
||||
resultsToAdd = [resultSet];
|
||||
} else {
|
||||
resultsToAdd = resultSet;
|
||||
resultsToAdd = resultSet.splice(0);
|
||||
}
|
||||
const sizeChanges = () => {
|
||||
this.tables.map(t => {
|
||||
t.state.canBeMaximized = this.tables.length > 1;
|
||||
});
|
||||
|
||||
this.maximumBodySize = this.tables.reduce((p, c) => {
|
||||
return p + c.maximumSize;
|
||||
}, 0);
|
||||
|
||||
if (this.state && this.state.scrollPosition) {
|
||||
this.splitView.setScrollPosition(this.state.scrollPosition);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.configurationService.getValue<boolean>('sql.results.streaming')) {
|
||||
this.addResultSet(resultsToAdd);
|
||||
sizeChanges();
|
||||
} else {
|
||||
resultsToAdd = resultsToAdd.filter(e => e.complete);
|
||||
if (resultsToAdd.length > 0) {
|
||||
this.addResultSet(resultsToAdd);
|
||||
}
|
||||
sizeChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private updateResultSet(resultSet: sqlops.ResultSetSummary | sqlops.ResultSetSummary[]) {
|
||||
let resultsToUpdate: sqlops.ResultSetSummary[];
|
||||
if (!Array.isArray(resultSet)) {
|
||||
resultsToUpdate = [resultSet];
|
||||
} else {
|
||||
resultsToUpdate = resultSet.splice(0);
|
||||
}
|
||||
|
||||
const sizeChanges = () => {
|
||||
this.maximumBodySize = this.tables.reduce((p, c) => {
|
||||
return p + c.maximumSize;
|
||||
}, 0);
|
||||
|
||||
if (this.state && this.state.scrollPosition) {
|
||||
this.splitView.setScrollPosition(this.state.scrollPosition);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.configurationService.getValue<boolean>('sql.results.streaming')) {
|
||||
for (let set of resultsToUpdate) {
|
||||
let table = this.tables.find(t => t.resultSet.batchId === set.batchId && t.resultSet.id === set.id);
|
||||
if (table) {
|
||||
table.updateResult(set);
|
||||
} else {
|
||||
warn('Got result set update request for non-existant table');
|
||||
}
|
||||
}
|
||||
sizeChanges();
|
||||
} else {
|
||||
resultsToUpdate = resultsToUpdate.filter(e => e.complete);
|
||||
if (resultsToUpdate.length > 0) {
|
||||
this.addResultSet(resultsToUpdate);
|
||||
}
|
||||
sizeChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private addResultSet(resultSet: sqlops.ResultSetSummary[]) {
|
||||
let tables: GridTable<any>[] = [];
|
||||
|
||||
for (let set of resultsToAdd) {
|
||||
for (let set of resultSet) {
|
||||
let tableState: GridTableState;
|
||||
if (this._state) {
|
||||
tableState = this.state.tableStates.find(e => e.batchId === set.batchId && e.resultId === set.id);
|
||||
@@ -420,6 +456,7 @@ class GridTable<T> extends Disposable implements IView {
|
||||
});
|
||||
this.dataProvider.dataRows = collection;
|
||||
this.table.updateRowCount();
|
||||
this.setupState();
|
||||
}
|
||||
|
||||
public onRemove() {
|
||||
@@ -513,12 +550,18 @@ class GridTable<T> extends Disposable implements IView {
|
||||
});
|
||||
|
||||
this.table.grid.onScroll.subscribe((e, data) => {
|
||||
if (!this.scrolled && this.state.scrollPosition && isInDOM(this.container)) {
|
||||
if (!this.visible) {
|
||||
// If the grid is not set up yet it can get scroll events resetting the top to 0px,
|
||||
// so ignore those events
|
||||
return;
|
||||
}
|
||||
if (!this.scrolled && (this.state.scrollPositionY || this.state.scrollPositionX) && isInDOM(this.container)) {
|
||||
this.scrolled = true;
|
||||
this.table.grid.scrollTo(this.state.scrollPosition);
|
||||
this.restoreScrollState();
|
||||
}
|
||||
if (this.state && isInDOM(this.container)) {
|
||||
this.state.scrollPosition = data.scrollTop;
|
||||
this.state.scrollPositionY = data.scrollTop;
|
||||
this.state.scrollPositionX = data.scrollLeft;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -527,8 +570,13 @@ class GridTable<T> extends Disposable implements IView {
|
||||
this.state.activeCell = this.table.grid.getActiveCell();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.setupState();
|
||||
private restoreScrollState() {
|
||||
if (this.state.scrollPositionX || this.state.scrollPositionY) {
|
||||
this.table.grid.scrollTo(this.state.scrollPositionY);
|
||||
this.table.grid.getContainerNode().children[3].scrollLeft = this.state.scrollPositionX;
|
||||
}
|
||||
}
|
||||
|
||||
private setupState() {
|
||||
@@ -537,20 +585,18 @@ class GridTable<T> extends Disposable implements IView {
|
||||
|
||||
this._register(this.state.onCanBeMaximizedChange(this.rebuildActionBar, this));
|
||||
|
||||
if (this.state.scrollPosition) {
|
||||
// most of the time this won't do anything
|
||||
this.table.grid.scrollTo(this.state.scrollPosition);
|
||||
// the problem here is that the scrolling state slickgrid uses
|
||||
// doesn't work with it offDOM.
|
||||
}
|
||||
this.restoreScrollState();
|
||||
|
||||
if (this.state.selection) {
|
||||
this.selectionModel.setSelectedRanges(this.state.selection);
|
||||
}
|
||||
// Setting the active cell resets the selection so save it here
|
||||
let savedSelection = this.state.selection;
|
||||
|
||||
if (this.state.activeCell) {
|
||||
this.table.setActiveCell(this.state.activeCell.row, this.state.activeCell.cell);
|
||||
}
|
||||
|
||||
if (savedSelection) {
|
||||
this.selectionModel.setSelectedRanges(savedSelection);
|
||||
}
|
||||
}
|
||||
|
||||
public get state(): GridTableState {
|
||||
@@ -601,6 +647,7 @@ class GridTable<T> extends Disposable implements IView {
|
||||
this.table.updateRowCount();
|
||||
}
|
||||
this.rowNumberColumn.updateRowCount(resultSet.rowCount);
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
|
||||
private generateContext(cell?: Slick.Cell): IGridActionContext {
|
||||
|
||||
@@ -282,6 +282,7 @@ export class QueryResultsView extends Disposable {
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
dispose(this.runnerDisposables);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,8 +387,14 @@ export class QueryModelService implements IQueryModelService {
|
||||
// We do not have a query runner for this editor, so create a new one
|
||||
// and map it to the results uri
|
||||
queryRunner = this._instantiationService.createInstance(QueryRunner, ownerUri);
|
||||
const resultSetEventType = 'resultSet';
|
||||
queryRunner.addListener(QREvents.RESULT_SET, resultSet => {
|
||||
this._fireQueryEvent(ownerUri, 'resultSet', resultSet);
|
||||
this._fireQueryEvent(ownerUri, resultSetEventType, resultSet);
|
||||
});
|
||||
queryRunner.onResultSetUpdate(resultSetSummaries => {
|
||||
resultSetSummaries.forEach(resultSet => {
|
||||
this._fireQueryEvent(ownerUri, resultSetEventType, resultSet);
|
||||
})
|
||||
});
|
||||
queryRunner.addListener(QREvents.BATCH_START, batch => {
|
||||
let link = undefined;
|
||||
|
||||
@@ -97,8 +97,8 @@ export default class QueryRunner extends Disposable {
|
||||
return l.concat(e);
|
||||
}
|
||||
});
|
||||
private _echoedResultSet = echo(this._debouncedResultSet.event);
|
||||
public readonly onResultSet = this._echoedResultSet.event;
|
||||
// private _echoedResultSet = echo(this._debouncedResultSet.event);
|
||||
public readonly onResultSet = this._debouncedResultSet.event;
|
||||
|
||||
private _onResultSetUpdate = this._register(new Emitter<sqlops.ResultSetSummary>());
|
||||
private _debouncedResultSetUpdate = debounceEvent<sqlops.ResultSetSummary, sqlops.ResultSetSummary[]>(this._onResultSetUpdate.event, (l, e) => {
|
||||
@@ -109,8 +109,8 @@ export default class QueryRunner extends Disposable {
|
||||
return l.concat(e);
|
||||
}
|
||||
});
|
||||
private _echoedResultSetUpdate = echo(this._debouncedResultSetUpdate.event);
|
||||
public readonly onResultSetUpdate = this._echoedResultSetUpdate.event;
|
||||
// private _echoedResultSetUpdate = echo(this._debouncedResultSetUpdate.event);
|
||||
public readonly onResultSetUpdate = this._debouncedResultSetUpdate.event;
|
||||
|
||||
private _onQueryStart = this._register(new Emitter<void>());
|
||||
public readonly onQueryStart: Event<void> = this._onQueryStart.event;
|
||||
@@ -203,9 +203,9 @@ export default class QueryRunner extends Disposable {
|
||||
return TPromise.as(undefined);
|
||||
}
|
||||
this._echoedMessages.clear();
|
||||
this._echoedResultSet.clear();
|
||||
// this._echoedResultSet.clear();
|
||||
this._debouncedMessage.clear();
|
||||
this._debouncedResultSet.clear();
|
||||
// this._debouncedResultSet.clear();
|
||||
this._planXml = new Deferred<string>();
|
||||
this._batchSets = [];
|
||||
this._hasCompleted = false;
|
||||
@@ -434,10 +434,10 @@ export default class QueryRunner extends Disposable {
|
||||
};
|
||||
|
||||
return this._queryManagementService.getQueryRows(rowData).then(r => r, error => {
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: nls.localize('query.gettingRowsFailedError', 'Something went wrong getting more rows: {0}', error)
|
||||
});
|
||||
// this._notificationService.notify({
|
||||
// severity: Severity.Error,
|
||||
// message: nls.localize('query.gettingRowsFailedError', 'Something went wrong getting more rows: {0}', error)
|
||||
// });
|
||||
return error;
|
||||
});
|
||||
}
|
||||
@@ -492,11 +492,11 @@ export default class QueryRunner extends Disposable {
|
||||
}
|
||||
resolve(result);
|
||||
}, error => {
|
||||
let errorMessage = nls.localize('query.moreRowsFailedError', 'Something went wrong getting more rows:');
|
||||
self._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: `${errorMessage} ${error}`
|
||||
});
|
||||
// let errorMessage = nls.localize('query.moreRowsFailedError', 'Something went wrong getting more rows:');
|
||||
// self._notificationService.notify({
|
||||
// severity: Severity.Error,
|
||||
// message: `${errorMessage} ${error}`
|
||||
// });
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -9,12 +9,13 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { localize } from 'vs/nls';
|
||||
import * as platform from 'vs/platform/registry/common/platform';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
|
||||
export const Extensions = {
|
||||
NotebookProviderContribution: 'notebook.providers'
|
||||
};
|
||||
|
||||
export interface NotebookProviderDescription {
|
||||
export interface NotebookProviderRegistration {
|
||||
provider: string;
|
||||
fileExtensions: string | string[];
|
||||
}
|
||||
@@ -54,42 +55,28 @@ let notebookContrib: IJSONSchema = {
|
||||
};
|
||||
|
||||
export interface INotebookProviderRegistry {
|
||||
registerNotebookProvider(provider: NotebookProviderDescription): void;
|
||||
getSupportedFileExtensions(): string[];
|
||||
getProviderForFileType(fileType: string): string;
|
||||
readonly registrations: NotebookProviderRegistration[];
|
||||
readonly onNewRegistration: Event<{ id: string, registration: NotebookProviderRegistration }>;
|
||||
|
||||
registerNotebookProvider(registration: NotebookProviderRegistration): void;
|
||||
}
|
||||
|
||||
class NotebookProviderRegistry implements INotebookProviderRegistry {
|
||||
private providerIdToProviders = new Map<string, NotebookProviderDescription>();
|
||||
private fileToProviders = new Map<string, NotebookProviderDescription>();
|
||||
private providerIdToRegistration = new Map<string, NotebookProviderRegistration>();
|
||||
private _onNewRegistration = new Emitter<{ id: string, registration: NotebookProviderRegistration }>();
|
||||
public readonly onNewRegistration: Event<{ id: string, registration: NotebookProviderRegistration }> = this._onNewRegistration.event;
|
||||
|
||||
registerNotebookProvider(provider: NotebookProviderDescription): void {
|
||||
registerNotebookProvider(registration: NotebookProviderRegistration): void {
|
||||
// Note: this method intentionally overrides default provider for a file type.
|
||||
// This means that any built-in provider will be overridden by registered extensions
|
||||
this.providerIdToProviders.set(provider.provider, provider);
|
||||
if (provider.fileExtensions) {
|
||||
if (Array.isArray<string>(provider.fileExtensions)) {
|
||||
for (let fileType of provider.fileExtensions) {
|
||||
this.addFileProvider(fileType, provider);
|
||||
}
|
||||
} else {
|
||||
this.addFileProvider(provider.fileExtensions, provider);
|
||||
}
|
||||
}
|
||||
this.providerIdToRegistration.set(registration.provider, registration);
|
||||
this._onNewRegistration.fire( { id: registration.provider, registration: registration });
|
||||
}
|
||||
|
||||
private addFileProvider(fileType: string, provider: NotebookProviderDescription) {
|
||||
this.fileToProviders.set(fileType.toUpperCase(), provider);
|
||||
}
|
||||
|
||||
getSupportedFileExtensions(): string[] {
|
||||
return Array.from(this.fileToProviders.keys());
|
||||
}
|
||||
|
||||
getProviderForFileType(fileType: string): string {
|
||||
fileType = fileType.toUpperCase();
|
||||
let provider = this.fileToProviders.get(fileType);
|
||||
return provider ? provider.provider : undefined;
|
||||
public get registrations(): NotebookProviderRegistration[] {
|
||||
let registrationArray: NotebookProviderRegistration[] = [];
|
||||
this.providerIdToRegistration.forEach(p => registrationArray.push(p));
|
||||
return registrationArray;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,15 +84,15 @@ const notebookProviderRegistry = new NotebookProviderRegistry();
|
||||
platform.Registry.add(Extensions.NotebookProviderContribution, notebookProviderRegistry);
|
||||
|
||||
|
||||
ExtensionsRegistry.registerExtensionPoint<NotebookProviderDescription | NotebookProviderDescription[]>(Extensions.NotebookProviderContribution, [], notebookContrib).setHandler(extensions => {
|
||||
ExtensionsRegistry.registerExtensionPoint<NotebookProviderRegistration | NotebookProviderRegistration[]>(Extensions.NotebookProviderContribution, [], notebookContrib).setHandler(extensions => {
|
||||
|
||||
function handleExtension(contrib: NotebookProviderDescription, extension: IExtensionPointUser<any>) {
|
||||
function handleExtension(contrib: NotebookProviderRegistration, extension: IExtensionPointUser<any>) {
|
||||
notebookProviderRegistry.registerNotebookProvider(contrib);
|
||||
}
|
||||
|
||||
for (let extension of extensions) {
|
||||
const { value } = extension;
|
||||
if (Array.isArray<NotebookProviderDescription>(value)) {
|
||||
if (Array.isArray<NotebookProviderRegistration>(value)) {
|
||||
for (let command of value) {
|
||||
handleExtension(command, extension);
|
||||
}
|
||||
|
||||
@@ -6,21 +6,33 @@
|
||||
'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';
|
||||
import { ICellModel, INotebookModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||
|
||||
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;
|
||||
|
||||
readonly onNotebookEditorAdd: Event<INotebookEditor>;
|
||||
readonly onNotebookEditorRemove: Event<INotebookEditor>;
|
||||
onNotebookEditorRename: Event<INotebookEditor>;
|
||||
|
||||
readonly isRegistrationComplete: boolean;
|
||||
readonly registrationComplete: Promise<void>;
|
||||
/**
|
||||
* Register a metadata provider
|
||||
*/
|
||||
@@ -31,6 +43,10 @@ export interface INotebookService {
|
||||
*/
|
||||
unregisterProvider(providerId: string): void;
|
||||
|
||||
getSupportedFileExtensions(): string[];
|
||||
|
||||
getProviderForFileType(fileType: string): string;
|
||||
|
||||
/**
|
||||
* Initializes and returns a Notebook manager that can handle all important calls to open, display, and
|
||||
* run cells in a notebook.
|
||||
@@ -40,11 +56,17 @@ 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;
|
||||
|
||||
getMimeRegistry(): RenderMimeRegistry;
|
||||
|
||||
renameNotebookEditor(oldUri: URI, newUri: URI, currentEditor: INotebookEditor): void;
|
||||
}
|
||||
|
||||
export interface INotebookProvider {
|
||||
@@ -62,8 +84,21 @@ 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;
|
||||
readonly cells?: ICellModel[];
|
||||
readonly modelReady: Promise<INotebookModel>;
|
||||
isDirty(): boolean;
|
||||
isActive(): boolean;
|
||||
isVisible(): boolean;
|
||||
save(): Promise<boolean>;
|
||||
executeEdits(edits: ISingleNotebookEditOperation[]): boolean;
|
||||
}
|
||||
@@ -10,43 +10,151 @@ 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 { Extensions, INotebookProviderRegistry, NotebookProviderRegistration } from 'sql/services/notebook/notebookRegistry';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Memento } from 'vs/workbench/common/memento';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IExtensionManagementService, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { getIdFromLocalExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
|
||||
const DEFAULT_NOTEBOOK_FILETYPE = 'IPYNB';
|
||||
export interface NotebookProviderProperties {
|
||||
provider: string;
|
||||
fileExtensions: string[];
|
||||
}
|
||||
|
||||
export class NotebookService implements INotebookService {
|
||||
interface NotebookProviderCache {
|
||||
[id: string]: NotebookProviderProperties;
|
||||
}
|
||||
|
||||
interface NotebookProvidersMemento {
|
||||
notebookProviderCache: NotebookProviderCache;
|
||||
}
|
||||
|
||||
const notebookRegistry = Registry.as<INotebookProviderRegistry>(Extensions.NotebookProviderContribution);
|
||||
|
||||
class ProviderDescriptor {
|
||||
private _instanceReady = new Deferred<INotebookProvider>();
|
||||
constructor(private providerId: string, private _instance?: INotebookProvider) {
|
||||
if (_instance) {
|
||||
this._instanceReady.resolve(_instance);
|
||||
}
|
||||
}
|
||||
|
||||
public get instanceReady(): Promise<INotebookProvider> {
|
||||
return this._instanceReady.promise;
|
||||
}
|
||||
|
||||
public get instance(): INotebookProvider {
|
||||
return this._instance;
|
||||
}
|
||||
public set instance(value: INotebookProvider) {
|
||||
this._instance = value;
|
||||
this._instanceReady.resolve(value);
|
||||
}
|
||||
}
|
||||
|
||||
export class NotebookService extends Disposable implements INotebookService {
|
||||
_serviceBrand: any;
|
||||
|
||||
private _memento = new Memento('notebookProviders');
|
||||
private _mimeRegistry: RenderMimeRegistry;
|
||||
private _providers: Map<string, INotebookProvider> = new Map();
|
||||
private _providers: Map<string, ProviderDescriptor> = new Map();
|
||||
private _managers: Map<string, INotebookManager> = new Map();
|
||||
private _onNotebookEditorAdd = new Emitter<INotebookEditor>();
|
||||
private _onNotebookEditorRemove = new Emitter<INotebookEditor>();
|
||||
private _onCellChanged = new Emitter<INotebookEditor>();
|
||||
private _onNotebookEditorRename = new Emitter<INotebookEditor>();
|
||||
private _editors = new Map<string, INotebookEditor>();
|
||||
private _fileToProviders = new Map<string, NotebookProviderRegistration>();
|
||||
private _registrationComplete = new Deferred<void>();
|
||||
private _isRegistrationComplete = false;
|
||||
|
||||
constructor() {
|
||||
constructor(
|
||||
@IStorageService private _storageService: IStorageService,
|
||||
@IExtensionService extensionService: IExtensionService,
|
||||
@IExtensionManagementService extensionManagementService: IExtensionManagementService
|
||||
) {
|
||||
super();
|
||||
this._register(notebookRegistry.onNewRegistration(this.updateRegisteredProviders, this));
|
||||
this.registerDefaultProvider();
|
||||
|
||||
if (extensionService) {
|
||||
extensionService.whenInstalledExtensionsRegistered().then(() => {
|
||||
this.cleanupProviders();
|
||||
this._isRegistrationComplete = true;
|
||||
this._registrationComplete.resolve();
|
||||
});
|
||||
}
|
||||
if (extensionManagementService) {
|
||||
this._register(extensionManagementService.onDidUninstallExtension(({ identifier }) => this.removeContributedProvidersFromCache(identifier, extensionService)));
|
||||
}
|
||||
}
|
||||
|
||||
private registerDefaultProvider() {
|
||||
let defaultProvider = new BuiltinProvider();
|
||||
this.registerProvider(defaultProvider.providerId, defaultProvider);
|
||||
let registry = Registry.as<INotebookProviderRegistry>(Extensions.NotebookProviderContribution);
|
||||
registry.registerNotebookProvider({
|
||||
provider: defaultProvider.providerId,
|
||||
fileExtensions: DEFAULT_NOTEBOOK_FILETYPE
|
||||
});
|
||||
private updateRegisteredProviders(p: { id: string; registration: NotebookProviderRegistration; }) {
|
||||
let registration = p.registration;
|
||||
|
||||
if (!this._providers.has(p.id)) {
|
||||
this._providers.set(p.id, new ProviderDescriptor(p.id));
|
||||
}
|
||||
if (registration.fileExtensions) {
|
||||
if (Array.isArray<string>(registration.fileExtensions)) {
|
||||
for (let fileType of registration.fileExtensions) {
|
||||
this.addFileProvider(fileType, registration);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.addFileProvider(registration.fileExtensions, registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerProvider(providerId: string, provider: INotebookProvider): void {
|
||||
this._providers.set(providerId, provider);
|
||||
registerProvider(providerId: string, instance: INotebookProvider): void {
|
||||
let providerDescriptor = this._providers.get(providerId);
|
||||
if (providerDescriptor) {
|
||||
// Update, which will resolve the promise for anyone waiting on the instance to be registered
|
||||
providerDescriptor.instance = instance;
|
||||
} else {
|
||||
this._providers.set(providerId, new ProviderDescriptor(providerId, instance));
|
||||
}
|
||||
}
|
||||
|
||||
unregisterProvider(providerId: string): void {
|
||||
this._providers.delete(providerId);
|
||||
}
|
||||
|
||||
get isRegistrationComplete(): boolean {
|
||||
return this._isRegistrationComplete;
|
||||
}
|
||||
|
||||
get registrationComplete(): Promise<void> {
|
||||
return this._registrationComplete.promise;
|
||||
}
|
||||
|
||||
private addFileProvider(fileType: string, provider: NotebookProviderRegistration) {
|
||||
this._fileToProviders.set(fileType.toUpperCase(), provider);
|
||||
}
|
||||
|
||||
getSupportedFileExtensions(): string[] {
|
||||
return Array.from(this._fileToProviders.keys());
|
||||
}
|
||||
|
||||
getProviderForFileType(fileType: string): string {
|
||||
fileType = fileType.toUpperCase();
|
||||
let provider = this._fileToProviders.get(fileType);
|
||||
return provider ? provider.provider : undefined;
|
||||
}
|
||||
|
||||
public shutdown(): void {
|
||||
this._managers.forEach(manager => {
|
||||
if (manager.serverManager) {
|
||||
@@ -71,27 +179,103 @@ 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;
|
||||
}
|
||||
get onCellChanged(): Event<INotebookEditor> {
|
||||
return this._onCellChanged.event;
|
||||
}
|
||||
|
||||
get onNotebookEditorRename(): Event<INotebookEditor> {
|
||||
return this._onNotebookEditorRename.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
|
||||
let uriString = notebookUri.toString();
|
||||
let manager = this._managers.get(uriString);
|
||||
if (manager) {
|
||||
this._managers.delete(uriString);
|
||||
let provider = this._providers.get(manager.providerId);
|
||||
provider.handleNotebookClosed(notebookUri);
|
||||
this.sendNotebookCloseToProvider(editor);
|
||||
}
|
||||
|
||||
listNotebookEditors(): INotebookEditor[] {
|
||||
let editors = [];
|
||||
this._editors.forEach(e => editors.push(e));
|
||||
return editors;
|
||||
}
|
||||
|
||||
renameNotebookEditor(oldUri: URI, newUri: URI, currentEditor: INotebookEditor): void {
|
||||
let oldUriKey = oldUri.toString();
|
||||
if(this._editors.has(oldUriKey))
|
||||
{
|
||||
this._editors.delete(oldUriKey);
|
||||
currentEditor.notebookParams.notebookUri = newUri;
|
||||
this._editors.set(newUri.toString(), currentEditor);
|
||||
this._onNotebookEditorRename.fire(currentEditor);
|
||||
}
|
||||
}
|
||||
|
||||
private sendNotebookCloseToProvider(editor: INotebookEditor): void {
|
||||
let notebookUri = editor.notebookParams.notebookUri;
|
||||
let uriString = notebookUri.toString();
|
||||
let manager = this._managers.get(uriString);
|
||||
if (manager) {
|
||||
// As we have a manager, we can assume provider is ready
|
||||
this._managers.delete(uriString);
|
||||
let provider = this._providers.get(manager.providerId);
|
||||
provider.instance.handleNotebookClosed(notebookUri);
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE HELPERS /////////////////////////////////////////////////////
|
||||
private doWithProvider<T>(providerId: string, op: (provider: INotebookProvider) => Thenable<T>): Thenable<T> {
|
||||
private async doWithProvider<T>(providerId: string, op: (provider: INotebookProvider) => Thenable<T>): Promise<T> {
|
||||
// Make sure the provider exists before attempting to retrieve accounts
|
||||
let provider = this._providers.get(providerId);
|
||||
if (!provider) {
|
||||
return Promise.reject(new Error(localize('notebookServiceNoProvider', 'Notebook provider does not exist'))).then();
|
||||
let provider: INotebookProvider = await this.getProviderInstance(providerId);
|
||||
return op(provider);
|
||||
}
|
||||
|
||||
private async getProviderInstance(providerId: string, timeout?: number): Promise<INotebookProvider> {
|
||||
let providerDescriptor = this._providers.get(providerId);
|
||||
let instance: INotebookProvider;
|
||||
|
||||
// Try get from actual provider, waiting on its registration
|
||||
if (providerDescriptor) {
|
||||
if (!providerDescriptor.instance) {
|
||||
instance = await this.waitOnProviderAvailability(providerDescriptor);
|
||||
} else {
|
||||
instance = providerDescriptor.instance;
|
||||
}
|
||||
}
|
||||
|
||||
return op(provider);
|
||||
// Fall back to default if this failed
|
||||
if (!instance) {
|
||||
providerDescriptor = this._providers.get(DEFAULT_NOTEBOOK_PROVIDER);
|
||||
instance = providerDescriptor ? providerDescriptor.instance : undefined;
|
||||
}
|
||||
|
||||
// Should never happen, but if default wasn't registered we should throw
|
||||
if (!instance) {
|
||||
throw new Error(localize('notebookServiceNoProvider', 'Notebook provider does not exist'));
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private waitOnProviderAvailability(providerDescriptor: ProviderDescriptor, timeout?: number): Promise<INotebookProvider> {
|
||||
// Wait up to 10 seconds for the provider to be registered
|
||||
timeout = timeout || 10000;
|
||||
let promises: Promise<INotebookProvider>[] = [
|
||||
providerDescriptor.instanceReady,
|
||||
new Promise<INotebookProvider>((resolve, reject) => setTimeout(() => resolve(), timeout))
|
||||
];
|
||||
return Promise.race(promises);
|
||||
}
|
||||
|
||||
//Returns an instantiation of RenderMimeRegistry class
|
||||
@@ -104,7 +288,40 @@ export class NotebookService implements INotebookService {
|
||||
return this._mimeRegistry;
|
||||
}
|
||||
|
||||
private get providersMemento(): NotebookProvidersMemento {
|
||||
return this._memento.getMemento(this._storageService) as NotebookProvidersMemento;
|
||||
}
|
||||
|
||||
private cleanupProviders(): void {
|
||||
let knownProviders = Object.keys(notebookRegistry.registrations);
|
||||
let cache = this.providersMemento.notebookProviderCache;
|
||||
for (let key in cache) {
|
||||
if (!knownProviders.includes(key)) {
|
||||
this._providers.delete(key);
|
||||
delete cache[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private registerDefaultProvider() {
|
||||
let defaultProvider = new BuiltinProvider();
|
||||
this.registerProvider(defaultProvider.providerId, defaultProvider);
|
||||
notebookRegistry.registerNotebookProvider({
|
||||
provider: defaultProvider.providerId,
|
||||
fileExtensions: DEFAULT_NOTEBOOK_FILETYPE
|
||||
});
|
||||
}
|
||||
|
||||
private removeContributedProvidersFromCache(identifier: IExtensionIdentifier, extensionService: IExtensionService) {
|
||||
let extensionid = getIdFromLocalExtensionId(identifier.id);
|
||||
extensionService.getExtensions().then(i => {
|
||||
let extension = i.find(c => c.id === extensionid);
|
||||
if (extension && extension.contributes['notebookProvider']) {
|
||||
let id = extension.contributes['notebookProvider'].providerId;
|
||||
delete this.providersMemento.notebookProviderCache[id];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
308
src/sql/sqlops.proposed.d.ts
vendored
308
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,283 @@ 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;
|
||||
uri?: vscode.Uri;
|
||||
}
|
||||
|
||||
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).
|
||||
*/
|
||||
cells: 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 +1714,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 +1726,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 +1767,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,7 @@ export class ExtHostNotebook implements ExtHostNotebookShape {
|
||||
|
||||
private readonly _proxy: MainThreadNotebookShape;
|
||||
private _adapters = new Map<number, Adapter>();
|
||||
|
||||
// Notebook URI to manager lookup.
|
||||
constructor(_mainContext: IMainContext) {
|
||||
this._proxy = _mainContext.getProxy(SqlMainContext.MainThreadNotebook);
|
||||
@@ -63,11 +65,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));
|
||||
}
|
||||
|
||||
|
||||
107
src/sql/workbench/api/node/extHostNotebookDocumentData.ts
Normal file
107
src/sql/workbench/api/node/extHostNotebookDocumentData.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, INotebookModelChangedData } 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 _isDisposed: boolean = false;
|
||||
|
||||
constructor(private readonly _proxy: MainThreadNotebookDocumentsAndEditorsShape,
|
||||
private readonly _uri: URI,
|
||||
private _providerId: string,
|
||||
private _isDirty: boolean,
|
||||
private _cells: sqlops.nb.NotebookCell[]
|
||||
) {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
public onModelChanged(data: INotebookModelChangedData) {
|
||||
if (data) {
|
||||
this._isDirty = data.isDirty;
|
||||
this._cells = data.cells;
|
||||
this._providerId = data.providerId;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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;
|
||||
}
|
||||
|
||||
}
|
||||
209
src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts
Normal file
209
src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, { UriComponents } 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, INotebookModelChangedData
|
||||
} 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 _onDidChangeVisibleNotebookEditors = new Emitter<ExtHostNotebookEditor[]>();
|
||||
private readonly _onDidChangeActiveNotebookEditor = new Emitter<ExtHostNotebookEditor>();
|
||||
private _onDidOpenNotebook = new Emitter<sqlops.nb.NotebookDocument>();
|
||||
private _onDidChangeNotebookCell = new Emitter<sqlops.nb.NotebookCellChangeEvent>();
|
||||
|
||||
readonly onDidChangeVisibleNotebookEditors: Event<ExtHostNotebookEditor[]> = this._onDidChangeVisibleNotebookEditors.event;
|
||||
readonly onDidChangeActiveNotebookEditor: Event<ExtHostNotebookEditor> = this._onDidChangeActiveNotebookEditor.event;
|
||||
readonly onDidOpenNotebookDocument: Event<sqlops.nb.NotebookDocument> = this._onDidOpenNotebook.event;
|
||||
readonly onDidChangeNotebookCell: Event<sqlops.nb.NotebookCellChangeEvent> = this._onDidChangeNotebookCell.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,
|
||||
data.cells
|
||||
);
|
||||
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 (removedDocuments) {
|
||||
// TODO add doc close event
|
||||
}
|
||||
if (addedDocuments) {
|
||||
addedDocuments.forEach(d => this._onDidOpenNotebook.fire(d.document));
|
||||
}
|
||||
|
||||
if (delta.removedEditors || delta.addedEditors) {
|
||||
this._onDidChangeVisibleNotebookEditors.fire(this.getAllEditors());
|
||||
}
|
||||
if (delta.newActiveEditor !== undefined) {
|
||||
this._onDidChangeActiveNotebookEditor.fire(this.getActiveEditor());
|
||||
}
|
||||
}
|
||||
|
||||
$acceptModelChanged(uriComponents: UriComponents, e: INotebookModelChangedData): void {
|
||||
const uri = URI.revive(uriComponents);
|
||||
const strURL = uri.toString();
|
||||
let data = this._documents.get(strURL);
|
||||
if (data) {
|
||||
data.onModelChanged(e);
|
||||
this._onDidChangeNotebookCell.fire({
|
||||
cells: data.document.cells,
|
||||
notebook: data.document,
|
||||
kind: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//#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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user