mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-25 18:47:14 -05:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a30878599 | ||
|
|
c8a8935db0 | ||
|
|
ec196f57bb | ||
|
|
f7809ec3a7 | ||
|
|
71d3ec3616 | ||
|
|
4a7cf8d870 | ||
|
|
4bf8836c0a | ||
|
|
1ca36ee29c | ||
|
|
3446ff88cf | ||
|
|
de5a91a13f | ||
|
|
814cd73019 | ||
|
|
c21611661b | ||
|
|
8f817ce689 | ||
|
|
971b5111e7 | ||
|
|
07069a64ae | ||
|
|
6acea51f12 | ||
|
|
7aa2dab307 | ||
|
|
3091be8f67 | ||
|
|
487531cc52 | ||
|
|
58bfcb4273 | ||
|
|
8d8be27f22 | ||
|
|
27a978cba5 | ||
|
|
71b4e6afa4 | ||
|
|
e1f3b19c0c | ||
|
|
649c2aa5a6 | ||
|
|
cac8cc99e1 | ||
|
|
cb162b16f2 | ||
|
|
86e54ce145 | ||
|
|
efd809971f | ||
|
|
38ae14cc4d | ||
|
|
c7e33a90fe | ||
|
|
5add835750 | ||
|
|
734c614cba | ||
|
|
f6b347fa62 | ||
|
|
08d2f3125e | ||
|
|
385c48dcad | ||
|
|
0926057bfe | ||
|
|
6912e3893e | ||
|
|
d3052657df | ||
|
|
a5ca4d8edf | ||
|
|
afb1ebebd5 | ||
|
|
a04a9eb5ad | ||
|
|
027badd21f | ||
|
|
1affc760e6 | ||
|
|
3ca72b7398 | ||
|
|
702dbddd78 | ||
|
|
8fbecc0227 | ||
|
|
421271acfa | ||
|
|
98af76b3ac | ||
|
|
3952fdbe2d | ||
|
|
bc13beaa85 | ||
|
|
59b2e706ca | ||
|
|
8bf835c531 | ||
|
|
087ed7c132 | ||
|
|
4c075df327 | ||
|
|
9ea8baca05 | ||
|
|
9b6784720e | ||
|
|
3761e1dd60 | ||
|
|
b3eb809550 | ||
|
|
cb72865dcc | ||
|
|
d646b4729b | ||
|
|
a2dd903d0d | ||
|
|
28ed378ee7 |
@@ -91,17 +91,17 @@ Filename: "{app}\{#ExeBasename}.exe"; Description: "{cm:LaunchProgram,{#NameLong
|
|||||||
#else
|
#else
|
||||||
#define SoftwareClassesRootKey "HKLM"
|
#define SoftwareClassesRootKey "HKLM"
|
||||||
#endif
|
#endif
|
||||||
Root: HKCR; Subkey: "{#RegValueName}SourceFile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,{#NameLong}}"; Flags: uninsdeletekey
|
Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,{#NameLong}}"; Flags: uninsdeletekey
|
||||||
Root: HKCR; Subkey: "{#RegValueName}SourceFile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"
|
Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"
|
||||||
Root: HKCR; Subkey: "{#RegValueName}SourceFile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""
|
Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""
|
||||||
Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}\bin"; Tasks: addtopath; Check: NeedsAddPath(ExpandConstant('{app}\bin'))
|
Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}\bin"; Tasks: addtopath; Check: NeedsAddPath(ExpandConstant('{app}\bin'))
|
||||||
|
|
||||||
Root: HKCU; Subkey: "Software\Classes\.sql\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles
|
Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sql\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles
|
||||||
Root: HKCU; Subkey: "Software\Classes\.sql\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sql"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles
|
Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sql\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sql"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles
|
||||||
Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SQL}"; Flags: uninsdeletekey; Tasks: associatewithfiles
|
Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SQL}"; Flags: uninsdeletekey; Tasks: associatewithfiles
|
||||||
Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles
|
Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles
|
||||||
Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles
|
Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\code_file.ico"; Tasks: associatewithfiles
|
||||||
Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles
|
Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles
|
||||||
; Environment
|
; Environment
|
||||||
#if "user" == InstallTarget
|
#if "user" == InstallTarget
|
||||||
#define EnvironmentRootKey "HKCU"
|
#define EnvironmentRootKey "HKCU"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "agent",
|
"name": "agent",
|
||||||
"displayName": "SQL Server Agent",
|
"displayName": "SQL Server Agent",
|
||||||
"description": "Manage and troubleshoot SQL Server Agent jobs",
|
"description": "Manage and troubleshoot SQL Server Agent jobs",
|
||||||
"version": "0.35.0",
|
"version": "0.35.2",
|
||||||
"publisher": "Microsoft",
|
"publisher": "Microsoft",
|
||||||
"preview": true,
|
"preview": true,
|
||||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",
|
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export class JobData implements IAgentDialogData {
|
|||||||
public jobSchedules: sqlops.AgentJobScheduleInfo[];
|
public jobSchedules: sqlops.AgentJobScheduleInfo[];
|
||||||
public alerts: sqlops.AgentAlertInfo[];
|
public alerts: sqlops.AgentAlertInfo[];
|
||||||
public jobId: string;
|
public jobId: string;
|
||||||
|
public startStepId: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
ownerUri: string,
|
ownerUri: string,
|
||||||
@@ -60,10 +61,11 @@ export class JobData implements IAgentDialogData {
|
|||||||
this.category = jobInfo.category;
|
this.category = jobInfo.category;
|
||||||
this.description = jobInfo.description;
|
this.description = jobInfo.description;
|
||||||
this.enabled = jobInfo.enabled;
|
this.enabled = jobInfo.enabled;
|
||||||
this.jobSteps = jobInfo.JobSteps;
|
this.jobSteps = jobInfo.jobSteps;
|
||||||
this.jobSchedules = jobInfo.JobSchedules;
|
this.jobSchedules = jobInfo.jobSchedules;
|
||||||
this.alerts = jobInfo.Alerts;
|
this.alerts = jobInfo.alerts;
|
||||||
this.jobId = jobInfo.jobId;
|
this.jobId = jobInfo.jobId;
|
||||||
|
this.startStepId = jobInfo.startStepId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,17 +143,17 @@ export class JobData implements IAgentDialogData {
|
|||||||
name: this.name,
|
name: this.name,
|
||||||
owner: this.owner,
|
owner: this.owner,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
EmailLevel: this.emailLevel,
|
emailLevel: this.emailLevel,
|
||||||
PageLevel: this.pageLevel,
|
pageLevel: this.pageLevel,
|
||||||
EventLogLevel: this.eventLogLevel,
|
eventLogLevel: this.eventLogLevel,
|
||||||
DeleteLevel: this.deleteLevel,
|
deleteLevel: this.deleteLevel,
|
||||||
OperatorToEmail: this.operatorToEmail,
|
operatorToEmail: this.operatorToEmail,
|
||||||
OperatorToPage: this.operatorToPage,
|
operatorToPage: this.operatorToPage,
|
||||||
enabled: this.enabled,
|
enabled: this.enabled,
|
||||||
category: this.category,
|
category: this.category,
|
||||||
Alerts: this.alerts,
|
alerts: this.alerts,
|
||||||
JobSchedules: this.jobSchedules,
|
jobSchedules: this.jobSchedules,
|
||||||
JobSteps: this.jobSteps,
|
jobSteps: this.jobSteps,
|
||||||
// The properties below are not collected from UI
|
// The properties below are not collected from UI
|
||||||
// We could consider using a seperate class for create job request
|
// 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
|
categoryType: 1, // LocalJob, hard-coding the value, corresponds to the target tab in SSMS
|
||||||
lastRun: '',
|
lastRun: '',
|
||||||
nextRun: '',
|
nextRun: '',
|
||||||
jobId: this.jobId
|
jobId: this.jobId,
|
||||||
|
startStepId: this.startStepId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,6 +123,7 @@ export class JobStepData implements IAgentDialogData {
|
|||||||
stepData.retryInterval = jobStepInfo.retryInterval,
|
stepData.retryInterval = jobStepInfo.retryInterval,
|
||||||
stepData.proxyName = jobStepInfo.proxyName;
|
stepData.proxyName = jobStepInfo.proxyName;
|
||||||
stepData.dialogMode = AgentDialogMode.EDIT;
|
stepData.dialogMode = AgentDialogMode.EDIT;
|
||||||
|
stepData.viaJobDialog = true;
|
||||||
return stepData;
|
return stepData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export abstract class AgentDialog<T extends IAgentDialogData> {
|
|||||||
public readonly onSuccess: vscode.Event<T> = this._onSuccess.event;
|
public readonly onSuccess: vscode.Event<T> = this._onSuccess.event;
|
||||||
public dialog: sqlops.window.modelviewdialog.Dialog;
|
public dialog: sqlops.window.modelviewdialog.Dialog;
|
||||||
|
|
||||||
|
// Dialog Name for Telemetry
|
||||||
|
public dialogName: string;
|
||||||
|
|
||||||
constructor(public ownerUri: string, public model: T, public title: 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);
|
protected abstract async initializeDialog(dialog: sqlops.window.modelviewdialog.Dialog);
|
||||||
|
|
||||||
public async openDialog() {
|
public async openDialog(dialogName?: string) {
|
||||||
this.dialog = sqlops.window.modelviewdialog.createDialog(this.title);
|
let event = dialogName ? dialogName : null;
|
||||||
|
this.dialog = sqlops.window.modelviewdialog.createDialog(this.title, event);
|
||||||
|
|
||||||
await this.model.initialize();
|
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 DelayMinutesTextBoxLabel: string = localize('alertDialog.DelayMinutes', 'Delay Minutes');
|
||||||
private static readonly DelaySecondsTextBoxLabel: string = localize('alertDialog.DelaySeconds', 'Delay Seconds');
|
private static readonly DelaySecondsTextBoxLabel: string = localize('alertDialog.DelaySeconds', 'Delay Seconds');
|
||||||
|
|
||||||
|
// Event Name strings
|
||||||
|
private readonly NewAlertDialog = 'NewAlertDialogOpen';
|
||||||
|
private readonly EditAlertDialog = 'EditAlertDialogOpened';
|
||||||
|
|
||||||
// UI Components
|
// UI Components
|
||||||
private generalTab: sqlops.window.modelviewdialog.DialogTab;
|
private generalTab: sqlops.window.modelviewdialog.DialogTab;
|
||||||
private responseTab: 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 delayMinutesTextBox: sqlops.InputBoxComponent;
|
||||||
private delaySecondsTextBox: sqlops.InputBoxComponent;
|
private delaySecondsTextBox: sqlops.InputBoxComponent;
|
||||||
|
|
||||||
|
private isEdit: boolean = false;
|
||||||
private databases: string[];
|
private databases: string[];
|
||||||
private jobModel: JobData;
|
private jobModel: JobData;
|
||||||
public jobId: string;
|
public jobId: string;
|
||||||
@@ -166,6 +171,8 @@ export class AlertDialog extends AgentDialog<AlertData> {
|
|||||||
this.jobModel = jobModel;
|
this.jobModel = jobModel;
|
||||||
this.jobId = this.jobId ? this.jobId : this.jobModel.jobId;
|
this.jobId = this.jobId ? this.jobId : this.jobModel.jobId;
|
||||||
this.jobName = this.jobName ? this.jobName : this.jobModel.name;
|
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) {
|
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_TypeColumnString: string = localize('jobDialog.type', 'Type');
|
||||||
private readonly StepsTable_SuccessColumnString: string = localize('jobDialog.onSuccess', 'On Success');
|
private readonly StepsTable_SuccessColumnString: string = localize('jobDialog.onSuccess', 'On Success');
|
||||||
private readonly StepsTable_FailureColumnString: string = localize('jobDialog.onFailure', 'On Failure');
|
private readonly StepsTable_FailureColumnString: string = localize('jobDialog.onFailure', 'On Failure');
|
||||||
private readonly NewStepButtonString: string = localize('jobDialog.new', 'New...');
|
private readonly NewStepButtonString: string = localize('jobDialog.new', 'New Step');
|
||||||
private readonly EditStepButtonString: string = localize('jobDialog.edit', 'Edit');
|
private readonly EditStepButtonString: string = localize('jobDialog.edit', 'Edit Step');
|
||||||
private readonly DeleteStepButtonString: string = localize('jobDialog.delete', 'Delete');
|
private readonly DeleteStepButtonString: string = localize('jobDialog.delete', 'Delete Step');
|
||||||
private readonly MoveStepUpButtonString: string = localize('jobDialog.moveUp', 'Move Step Up');
|
private readonly MoveStepUpButtonString: string = localize('jobDialog.moveUp', 'Move Step Up');
|
||||||
private readonly MoveStepDownButtonString: string = localize('jobDialog.moveDown', '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
|
// Notifications tab strings
|
||||||
private readonly NotificationsTabTopLabelString: string = localize('jobDialog.notificationsTabTop', 'Actions to perform when the job completes');
|
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 AlertEnabledLabelString: string = localize('jobDialog.alertEnabledLabel', 'Enabled');
|
||||||
private readonly AlertTypeLabelString: string = localize('jobDialog.alertTypeLabel', 'Type');
|
private readonly AlertTypeLabelString: string = localize('jobDialog.alertTypeLabel', 'Type');
|
||||||
|
|
||||||
|
// Event Name strings
|
||||||
|
private readonly NewJobDialogEvent: string = 'NewJobDialogOpened';
|
||||||
|
private readonly EditJobDialogEvent: string = 'EditJobDialogOpened';
|
||||||
|
|
||||||
// UI Components
|
// UI Components
|
||||||
private generalTab: sqlops.window.modelviewdialog.DialogTab;
|
private generalTab: sqlops.window.modelviewdialog.DialogTab;
|
||||||
private stepsTab: 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 eventLogConditionDropdown: sqlops.DropDownComponent;
|
||||||
private deleteJobCheckBox: sqlops.CheckBoxComponent;
|
private deleteJobCheckBox: sqlops.CheckBoxComponent;
|
||||||
private deleteJobConditionDropdown: sqlops.DropDownComponent;
|
private deleteJobConditionDropdown: sqlops.DropDownComponent;
|
||||||
|
private startStepDropdown: sqlops.DropDownComponent;
|
||||||
|
|
||||||
// Schedule tab controls
|
// Schedule tab controls
|
||||||
private schedulesTable: sqlops.TableComponent;
|
private schedulesTable: sqlops.TableComponent;
|
||||||
@@ -115,6 +121,7 @@ export class JobDialog extends AgentDialog<JobData> {
|
|||||||
private steps: sqlops.AgentJobStepInfo[];
|
private steps: sqlops.AgentJobStepInfo[];
|
||||||
private schedules: sqlops.AgentJobScheduleInfo[];
|
private schedules: sqlops.AgentJobScheduleInfo[];
|
||||||
private alerts: sqlops.AgentAlertInfo[] = [];
|
private alerts: sqlops.AgentAlertInfo[] = [];
|
||||||
|
private startStepDropdownValues: sqlops.CategoryValue[] = [];
|
||||||
|
|
||||||
constructor(ownerUri: string, jobInfo: sqlops.AgentJobInfo = undefined) {
|
constructor(ownerUri: string, jobInfo: sqlops.AgentJobInfo = undefined) {
|
||||||
super(
|
super(
|
||||||
@@ -125,6 +132,7 @@ export class JobDialog extends AgentDialog<JobData> {
|
|||||||
this.schedules = this.model.jobSchedules ? this.model.jobSchedules : [];
|
this.schedules = this.model.jobSchedules ? this.model.jobSchedules : [];
|
||||||
this.alerts = this.model.alerts ? this.model.alerts : [];
|
this.alerts = this.model.alerts ? this.model.alerts : [];
|
||||||
this.isEdit = jobInfo ? true : false;
|
this.isEdit = jobInfo ? true : false;
|
||||||
|
this.dialogName = this.isEdit ? this.EditJobDialogEvent : this.NewJobDialogEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async initializeDialog() {
|
protected async initializeDialog() {
|
||||||
@@ -218,19 +226,26 @@ export class JobDialog extends AgentDialog<JobData> {
|
|||||||
this.StepsTable_FailureColumnString
|
this.StepsTable_FailureColumnString
|
||||||
],
|
],
|
||||||
data: data,
|
data: data,
|
||||||
height: 750
|
height: 650
|
||||||
}).component();
|
}).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()
|
this.moveStepUpButton = view.modelBuilder.button()
|
||||||
.withProperties({
|
.withProperties({
|
||||||
label: this.MoveStepUpButtonString,
|
label: this.MoveStepUpButtonString,
|
||||||
width: 80
|
width: 120
|
||||||
}).component();
|
}).component();
|
||||||
|
|
||||||
this.moveStepDownButton = view.modelBuilder.button()
|
this.moveStepDownButton = view.modelBuilder.button()
|
||||||
.withProperties({
|
.withProperties({
|
||||||
label: this.MoveStepDownButtonString,
|
label: this.MoveStepDownButtonString,
|
||||||
width: 80
|
width: 120
|
||||||
}).component();
|
}).component();
|
||||||
|
|
||||||
this.moveStepUpButton.enabled = false;
|
this.moveStepUpButton.enabled = false;
|
||||||
@@ -238,7 +253,7 @@ export class JobDialog extends AgentDialog<JobData> {
|
|||||||
|
|
||||||
this.newStepButton = view.modelBuilder.button().withProperties({
|
this.newStepButton = view.modelBuilder.button().withProperties({
|
||||||
label: this.NewStepButtonString,
|
label: this.NewStepButtonString,
|
||||||
width: 80
|
width: 140
|
||||||
}).component();
|
}).component();
|
||||||
|
|
||||||
let stepDialog = new JobStepDialog(this.model.ownerUri, '' , this.model, null, true);
|
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);
|
let stepInfo = JobStepData.convertToAgentJobStepInfo(step);
|
||||||
this.steps.push(stepInfo);
|
this.steps.push(stepInfo);
|
||||||
this.stepsTable.data = this.convertStepsToData(this.steps);
|
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)=>{
|
this.newStepButton.onDidClick((e)=>{
|
||||||
if (this.nameTextBox.value && this.nameTextBox.value.length > 0) {
|
if (this.nameTextBox.value && this.nameTextBox.value.length > 0) {
|
||||||
@@ -258,53 +278,127 @@ export class JobDialog extends AgentDialog<JobData> {
|
|||||||
|
|
||||||
this.editStepButton = view.modelBuilder.button().withProperties({
|
this.editStepButton = view.modelBuilder.button().withProperties({
|
||||||
label: this.EditStepButtonString,
|
label: this.EditStepButtonString,
|
||||||
width: 80
|
width: 140
|
||||||
}).component();
|
}).component();
|
||||||
|
|
||||||
this.deleteStepButton = view.modelBuilder.button().withProperties({
|
this.deleteStepButton = view.modelBuilder.button().withProperties({
|
||||||
label: this.DeleteStepButtonString,
|
label: this.DeleteStepButtonString,
|
||||||
width: 80
|
width: 140
|
||||||
}).component();
|
}).component();
|
||||||
|
|
||||||
this.stepsTable.enabled = false;
|
this.stepsTable.enabled = false;
|
||||||
this.editStepButton.enabled = false;
|
this.editStepButton.enabled = false;
|
||||||
this.deleteStepButton.enabled = false;
|
this.deleteStepButton.enabled = false;
|
||||||
|
|
||||||
this.stepsTable.onRowSelected(() => {
|
this.moveStepUpButton.onDidClick(() => {
|
||||||
// only let edit or delete steps if there's
|
let rowNumber = this.stepsTable.selectedRows[0];
|
||||||
// one step selection
|
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(() => {
|
||||||
|
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(() => {
|
||||||
if (this.stepsTable.selectedRows.length === 1) {
|
if (this.stepsTable.selectedRows.length === 1) {
|
||||||
let rowNumber = this.stepsTable.selectedRows[0];
|
let rowNumber = this.stepsTable.selectedRows[0];
|
||||||
let stepData = this.model.jobSteps[rowNumber];
|
let stepData = this.model.jobSteps[rowNumber];
|
||||||
this.deleteStepButton.enabled = true;
|
let editStepDialog = new JobStepDialog(this.model.ownerUri, '' , this.model, stepData, true);
|
||||||
this.editStepButton.enabled = true;
|
editStepDialog.onSuccess((step) => {
|
||||||
this.editStepButton.onDidClick(() => {
|
let stepInfo = JobStepData.convertToAgentJobStepInfo(step);
|
||||||
let stepDialog = new JobStepDialog(this.model.ownerUri, '' , this.model, stepData, true);
|
for (let i = 0; i < this.steps.length; i++) {
|
||||||
stepDialog.openDialog();
|
if (this.steps[i].id === stepInfo.id) {
|
||||||
});
|
this.steps[i] = 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.deleteStepButton.onDidClick(() => {
|
});
|
||||||
AgentUtils.getAgentService().then((agentService) => {
|
editStepDialog.openDialog();
|
||||||
let steps = this.model.jobSteps ? this.model.jobSteps : [];
|
}
|
||||||
agentService.deleteJobStep(this.ownerUri, stepData).then((result) => {
|
});
|
||||||
if (result && result.success) {
|
|
||||||
delete steps[rowNumber];
|
this.deleteStepButton.onDidClick(() => {
|
||||||
let data = this.convertStepsToData(steps);
|
if (this.stepsTable.selectedRows.length === 1) {
|
||||||
this.stepsTable.data = data;
|
let rowNumber = this.stepsTable.selectedRows[0];
|
||||||
}
|
AgentUtils.getAgentService().then((agentService) => {
|
||||||
});
|
let steps = this.model.jobSteps ? this.model.jobSteps : [];
|
||||||
|
let stepData = this.model.jobSteps[rowNumber];
|
||||||
|
agentService.deleteJobStep(this.ownerUri, stepData).then((result) => {
|
||||||
|
if (result && result.success) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let formModel = view.modelBuilder.formContainer()
|
this.stepsTable.onRowSelected((row) => {
|
||||||
.withFormItems([{
|
// only let edit or delete steps if there's
|
||||||
|
// one step selection
|
||||||
|
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) {
|
||||||
|
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 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,
|
component: this.stepsTable,
|
||||||
title: this.JobStepsTopLabelString,
|
title: this.JobStepsTopLabelString
|
||||||
actions: [this.moveStepUpButton, this.moveStepDownButton, this.newStepButton, this.editStepButton, this.deleteStepButton]
|
},
|
||||||
}]).withLayout({ width: '100%' }).component();
|
{
|
||||||
|
component: stepMoveContainer,
|
||||||
|
title: this.StartStepDropdownString
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: stepsDialogContainer,
|
||||||
|
title: ''
|
||||||
|
}
|
||||||
|
]).withLayout({ width: '100%' }).component();
|
||||||
await view.initializeModel(formModel);
|
await view.initializeModel(formModel);
|
||||||
|
this.setConditionDropdownSelectedValue(this.startStepDropdown, this.model.startStepId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,6 +661,7 @@ export class JobDialog extends AgentDialog<JobData> {
|
|||||||
this.model.pageLevel = this.getActualConditionValue(this.pagerCheckBox, this.pagerConditionDropdown);
|
this.model.pageLevel = this.getActualConditionValue(this.pagerCheckBox, this.pagerConditionDropdown);
|
||||||
this.model.eventLogLevel = this.getActualConditionValue(this.eventLogCheckBox, this.eventLogConditionDropdown);
|
this.model.eventLogLevel = this.getActualConditionValue(this.eventLogCheckBox, this.eventLogConditionDropdown);
|
||||||
this.model.deleteLevel = this.getActualConditionValue(this.deleteJobCheckBox, this.deleteJobConditionDropdown);
|
this.model.deleteLevel = this.getActualConditionValue(this.deleteJobCheckBox, this.deleteJobConditionDropdown);
|
||||||
|
this.model.startStepId = +this.getDropdownValue(this.startStepDropdown);
|
||||||
if (!this.model.jobSteps) {
|
if (!this.model.jobSteps) {
|
||||||
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 QuitJobReportingSuccess: string = localize('jobStepDialog.quitJobSuccess', 'Quit the job reporting success');
|
||||||
private readonly QuitJobReportingFailure: string = localize('jobStepDialog.quitJobFailure', 'Quit the job reporting failure');
|
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
|
// UI Components
|
||||||
|
|
||||||
// Dialogs
|
// Dialogs
|
||||||
@@ -131,6 +134,7 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
|||||||
this.jobModel = jobModel;
|
this.jobModel = jobModel;
|
||||||
this.jobName = this.jobName ? this.jobName : this.jobModel.name;
|
this.jobName = this.jobName ? this.jobName : this.jobModel.name;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
|
this.dialogName = this.isEdit ? this.EditStepDialog : this.NewStepDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeUIComponents() {
|
private initializeUIComponents() {
|
||||||
@@ -519,6 +523,7 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
|||||||
this.model.failureAction = this.failureActionDropdown.value as string;
|
this.model.failureAction = this.failureActionDropdown.value as string;
|
||||||
this.model.outputFileName = this.outputFileNameBox.value;
|
this.model.outputFileName = this.outputFileNameBox.value;
|
||||||
this.model.appendToLogFile = this.appendToExistingFileCheckbox.checked;
|
this.model.appendToLogFile = this.appendToExistingFileCheckbox.checked;
|
||||||
|
this.model.command = this.commandTextBox.value ? this.commandTextBox.value : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public async initializeDialog() {
|
public async initializeDialog() {
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ export class OperatorDialog extends AgentDialog<OperatorData> {
|
|||||||
private static readonly AlertEmailColumnLabel: string = localize('createOperator.AlertEmailColumnLabel', 'E-mail');
|
private static readonly AlertEmailColumnLabel: string = localize('createOperator.AlertEmailColumnLabel', 'E-mail');
|
||||||
private static readonly AlertPagerColumnLabel: string = localize('createOperator.AlertPagerColumnLabel', 'Pager');
|
private static readonly AlertPagerColumnLabel: string = localize('createOperator.AlertPagerColumnLabel', 'Pager');
|
||||||
|
|
||||||
|
// Event strings
|
||||||
|
private readonly NewOperatorDialog = 'NewOperatorDialogOpened';
|
||||||
|
private readonly EditOperatorDialog = 'EditOperatorDialogOpened';
|
||||||
|
|
||||||
// UI Components
|
// UI Components
|
||||||
private generalTab: sqlops.window.modelviewdialog.DialogTab;
|
private generalTab: sqlops.window.modelviewdialog.DialogTab;
|
||||||
private notificationsTab: sqlops.window.modelviewdialog.DialogTab;
|
private notificationsTab: sqlops.window.modelviewdialog.DialogTab;
|
||||||
@@ -68,12 +72,15 @@ export class OperatorDialog extends AgentDialog<OperatorData> {
|
|||||||
|
|
||||||
// Notification tab controls
|
// Notification tab controls
|
||||||
private alertsTable: sqlops.TableComponent;
|
private alertsTable: sqlops.TableComponent;
|
||||||
|
private isEdit: boolean = false;
|
||||||
|
|
||||||
constructor(ownerUri: string, operatorInfo: sqlops.AgentOperatorInfo = undefined) {
|
constructor(ownerUri: string, operatorInfo: sqlops.AgentOperatorInfo = undefined) {
|
||||||
super(
|
super(
|
||||||
ownerUri,
|
ownerUri,
|
||||||
new OperatorData(ownerUri, operatorInfo),
|
new OperatorData(ownerUri, operatorInfo),
|
||||||
operatorInfo ? OperatorDialog.EditDialogTitle : OperatorDialog.CreateDialogTitle);
|
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) {
|
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 PowerShellLabel: string = localize('createProxy.PowerShell', 'PowerShell');
|
||||||
private static readonly SubSystemHeadingLabel: string = localize('createProxy.subSystemHeading', 'Active to the following subsytems');
|
private static readonly SubSystemHeadingLabel: string = localize('createProxy.subSystemHeading', 'Active to the following subsytems');
|
||||||
|
|
||||||
|
private readonly NewProxyDialog = 'NewProxyDialogOpened';
|
||||||
|
private readonly EditProxyDialog = 'EditProxyDialogOpened';
|
||||||
|
|
||||||
// UI Components
|
// UI Components
|
||||||
private generalTab: sqlops.window.modelviewdialog.DialogTab;
|
private generalTab: sqlops.window.modelviewdialog.DialogTab;
|
||||||
|
|
||||||
@@ -56,6 +59,7 @@ export class ProxyDialog extends AgentDialog<ProxyData> {
|
|||||||
private powershellCheckBox: sqlops.CheckBoxComponent;
|
private powershellCheckBox: sqlops.CheckBoxComponent;
|
||||||
|
|
||||||
private credentials: sqlops.CredentialInfo[];
|
private credentials: sqlops.CredentialInfo[];
|
||||||
|
private isEdit: boolean = false;
|
||||||
|
|
||||||
constructor(ownerUri: string, proxyInfo: sqlops.AgentProxyInfo = undefined, credentials: sqlops.CredentialInfo[]) {
|
constructor(ownerUri: string, proxyInfo: sqlops.AgentProxyInfo = undefined, credentials: sqlops.CredentialInfo[]) {
|
||||||
super(
|
super(
|
||||||
@@ -63,6 +67,8 @@ export class ProxyDialog extends AgentDialog<ProxyData> {
|
|||||||
new ProxyData(ownerUri, proxyInfo),
|
new ProxyData(ownerUri, proxyInfo),
|
||||||
proxyInfo ? ProxyDialog.EditDialogTitle : ProxyDialog.CreateDialogTitle);
|
proxyInfo ? ProxyDialog.EditDialogTitle : ProxyDialog.CreateDialogTitle);
|
||||||
this.credentials = credentials;
|
this.credentials = credentials;
|
||||||
|
this.isEdit = proxyInfo ? true : false;
|
||||||
|
this.dialogName = this.isEdit ? this.EditProxyDialog : this.NewProxyDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async initializeDialog(dialog: sqlops.window.modelviewdialog.Dialog) {
|
protected async initializeDialog(dialog: sqlops.window.modelviewdialog.Dialog) {
|
||||||
|
|||||||
@@ -40,13 +40,13 @@ export class MainController {
|
|||||||
public activate(): void {
|
public activate(): void {
|
||||||
vscode.commands.registerCommand('agent.openJobDialog', (ownerUri: string, jobInfo: sqlops.AgentJobInfo) => {
|
vscode.commands.registerCommand('agent.openJobDialog', (ownerUri: string, jobInfo: sqlops.AgentJobInfo) => {
|
||||||
let dialog = new JobDialog(ownerUri, jobInfo);
|
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) => {
|
vscode.commands.registerCommand('agent.openNewStepDialog', (ownerUri: string, server: string, jobInfo: sqlops.AgentJobInfo, jobStepInfo: sqlops.AgentJobStepInfo) => {
|
||||||
AgentUtils.getAgentService().then((agentService) => {
|
AgentUtils.getAgentService().then((agentService) => {
|
||||||
let jobData: JobData = new JobData(ownerUri, jobInfo, agentService);
|
let jobData: JobData = new JobData(ownerUri, jobInfo, agentService);
|
||||||
let dialog = new JobStepDialog(ownerUri, server, jobData, jobStepInfo, false);
|
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) => {
|
vscode.commands.registerCommand('agent.openPickScheduleDialog', (ownerUri: string, jobName: string) => {
|
||||||
@@ -57,17 +57,16 @@ export class MainController {
|
|||||||
AgentUtils.getAgentService().then((agentService) => {
|
AgentUtils.getAgentService().then((agentService) => {
|
||||||
let jobData: JobData = new JobData(ownerUri, jobInfo, agentService);
|
let jobData: JobData = new JobData(ownerUri, jobInfo, agentService);
|
||||||
let dialog = new AlertDialog(ownerUri, jobData, alertInfo, false);
|
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) => {
|
vscode.commands.registerCommand('agent.openOperatorDialog', (ownerUri: string, operatorInfo: sqlops.AgentOperatorInfo) => {
|
||||||
let dialog = new OperatorDialog(ownerUri, operatorInfo);
|
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[]) => {
|
vscode.commands.registerCommand('agent.openProxyDialog', (ownerUri: string, proxyInfo: sqlops.AgentProxyInfo, credentials: sqlops.CredentialInfo[]) => {
|
||||||
let dialog = new ProxyDialog(ownerUri, proxyInfo, credentials);
|
let dialog = new ProxyDialog(ownerUri, proxyInfo, credentials);
|
||||||
dialog.openDialog();
|
dialog.dialogName ? dialog.openDialog(dialog.dialogName) : dialog.openDialog();
|
||||||
MainController.showNotYetImplemented();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ export class AzureAccountProvider implements sqlops.AccountProvider {
|
|||||||
return this._tokenCache.clear();
|
return this._tokenCache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSecurityToken(account: AzureAccount): Thenable<AzureAccountSecurityTokenCollection> {
|
public getSecurityToken(account: AzureAccount, resource: sqlops.AzureResource): Thenable<AzureAccountSecurityTokenCollection> {
|
||||||
return this.doIfInitialized(() => this.getAccessTokens(account));
|
return this.doIfInitialized(() => this.getAccessTokens(account, resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
public initialize(restoredAccounts: sqlops.Account[]): Thenable<sqlops.Account[]> {
|
public initialize(restoredAccounts: sqlops.Account[]): Thenable<sqlops.Account[]> {
|
||||||
@@ -90,7 +90,7 @@ export class AzureAccountProvider implements sqlops.AccountProvider {
|
|||||||
|
|
||||||
// Attempt to get fresh tokens. If this fails then the account is stale.
|
// Attempt to get fresh tokens. If this fails then the account is stale.
|
||||||
// NOTE: Based on ADAL implementation, getting tokens should use the refresh token if necessary
|
// NOTE: Based on ADAL implementation, getting tokens should use the refresh token if necessary
|
||||||
let task = this.getAccessTokens(account)
|
let task = this.getAccessTokens(account, sqlops.AzureResource.ResourceManagement)
|
||||||
.then(
|
.then(
|
||||||
() => {
|
() => {
|
||||||
return account;
|
return account;
|
||||||
@@ -161,9 +161,14 @@ export class AzureAccountProvider implements sqlops.AccountProvider {
|
|||||||
: Promise.reject(localize('accountProviderNotInitialized', 'Account provider not initialized, cannot perform action'));
|
: Promise.reject(localize('accountProviderNotInitialized', 'Account provider not initialized, cannot perform action'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAccessTokens(account: AzureAccount): Thenable<AzureAccountSecurityTokenCollection> {
|
private getAccessTokens(account: AzureAccount, resource: sqlops.AzureResource): Thenable<AzureAccountSecurityTokenCollection> {
|
||||||
let self = this;
|
let self = this;
|
||||||
|
|
||||||
|
const resourceIdMap = new Map<sqlops.AzureResource, string>([
|
||||||
|
[sqlops.AzureResource.ResourceManagement, self._metadata.settings.armResource.id],
|
||||||
|
[sqlops.AzureResource.Sql, self._metadata.settings.sqlResource.id]
|
||||||
|
]);
|
||||||
|
|
||||||
let accessTokenPromises: Thenable<void>[] = [];
|
let accessTokenPromises: Thenable<void>[] = [];
|
||||||
let tokenCollection: AzureAccountSecurityTokenCollection = {};
|
let tokenCollection: AzureAccountSecurityTokenCollection = {};
|
||||||
for (let tenant of account.properties.tenants) {
|
for (let tenant of account.properties.tenants) {
|
||||||
@@ -172,7 +177,7 @@ export class AzureAccountProvider implements sqlops.AccountProvider {
|
|||||||
let context = new adal.AuthenticationContext(authorityUrl, null, self._tokenCache);
|
let context = new adal.AuthenticationContext(authorityUrl, null, self._tokenCache);
|
||||||
|
|
||||||
context.acquireToken(
|
context.acquireToken(
|
||||||
self._metadata.settings.armResource.id,
|
resourceIdMap.get(resource),
|
||||||
tenant.userId,
|
tenant.userId,
|
||||||
self._metadata.settings.clientId,
|
self._metadata.settings.clientId,
|
||||||
(error: Error, response: adal.TokenResponse | adal.ErrorResponse) => {
|
(error: Error, response: adal.TokenResponse | adal.ErrorResponse) => {
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ export interface Settings {
|
|||||||
*/
|
*/
|
||||||
armResource?: Resource;
|
armResource?: Resource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information that describes the SQL Azure resource
|
||||||
|
*/
|
||||||
|
sqlResource?: Resource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of tenant IDs to authenticate against. If defined, then these IDs will be used
|
* A list of tenant IDs to authenticate against. If defined, then these IDs will be used
|
||||||
* instead of querying the tenants endpoint of the armResource
|
* instead of querying the tenants endpoint of the armResource
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ const publicAzureSettings: ProviderSettings = {
|
|||||||
id: 'https://management.core.windows.net/',
|
id: 'https://management.core.windows.net/',
|
||||||
endpoint: 'https://management.azure.com'
|
endpoint: 'https://management.azure.com'
|
||||||
},
|
},
|
||||||
|
sqlResource: {
|
||||||
|
id: 'https://database.windows.net/',
|
||||||
|
endpoint: 'https://database.windows.net'
|
||||||
|
},
|
||||||
redirectUri: 'http://localhost/redirect'
|
redirectUri: 'http://localhost/redirect'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,22 +223,16 @@ export default class TokenCache implements adal.TokenCache {
|
|||||||
return this.getOrCreateEncryptionParams()
|
return this.getOrCreateEncryptionParams()
|
||||||
.then(encryptionParams => {
|
.then(encryptionParams => {
|
||||||
try {
|
try {
|
||||||
let cacheCipher = fs.readFileSync(self._cacheSerializationPath, TokenCache.FsOptions);
|
return self.decryptCache('utf8', encryptionParams);
|
||||||
|
|
||||||
let decipher = crypto.createDecipheriv(TokenCache.CipherAlgorithm, encryptionParams.key, encryptionParams.initializationVector);
|
|
||||||
let cacheJson = decipher.update(cacheCipher, 'hex', 'binary');
|
|
||||||
cacheJson += decipher.final('binary');
|
|
||||||
|
|
||||||
// Deserialize the JSON into the array of tokens
|
|
||||||
let cacheObj = <adal.TokenResponse[]>JSON.parse(cacheJson);
|
|
||||||
for (let objIndex in cacheObj) {
|
|
||||||
// Rehydrate Date objects since they will always serialize as a string
|
|
||||||
cacheObj[objIndex].expiresOn = new Date(<string>cacheObj[objIndex].expiresOn);
|
|
||||||
}
|
|
||||||
|
|
||||||
return cacheObj;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e;
|
try {
|
||||||
|
// try to parse using 'binary' encoding and rewrite cache as UTF8
|
||||||
|
let response = self.decryptCache('binary', encryptionParams);
|
||||||
|
self.writeCache(response);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(null, err => {
|
.then(null, err => {
|
||||||
@@ -248,6 +242,22 @@ export default class TokenCache implements adal.TokenCache {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private decryptCache(encoding: crypto.Utf8AsciiBinaryEncoding, encryptionParams: EncryptionParams): adal.TokenResponse[] {
|
||||||
|
let cacheCipher = fs.readFileSync(this._cacheSerializationPath, TokenCache.FsOptions);
|
||||||
|
let decipher = crypto.createDecipheriv(TokenCache.CipherAlgorithm, encryptionParams.key, encryptionParams.initializationVector);
|
||||||
|
let cacheJson = decipher.update(cacheCipher, 'hex', encoding);
|
||||||
|
cacheJson += decipher.final(encoding);
|
||||||
|
|
||||||
|
// Deserialize the JSON into the array of tokens
|
||||||
|
let cacheObj = <adal.TokenResponse[]>JSON.parse(cacheJson);
|
||||||
|
for (let objIndex in cacheObj) {
|
||||||
|
// Rehydrate Date objects since they will always serialize as a string
|
||||||
|
cacheObj[objIndex].expiresOn = new Date(<string>cacheObj[objIndex].expiresOn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cacheObj;
|
||||||
|
}
|
||||||
|
|
||||||
private removeFromCache(cache: adal.TokenResponse[], entries: adal.TokenResponse[]): adal.TokenResponse[] {
|
private removeFromCache(cache: adal.TokenResponse[], entries: adal.TokenResponse[]): adal.TokenResponse[] {
|
||||||
entries.forEach((entry: adal.TokenResponse) => {
|
entries.forEach((entry: adal.TokenResponse) => {
|
||||||
// Check to see if the entry exists
|
// Check to see if the entry exists
|
||||||
@@ -274,7 +284,7 @@ export default class TokenCache implements adal.TokenCache {
|
|||||||
let cacheJson = JSON.stringify(cache);
|
let cacheJson = JSON.stringify(cache);
|
||||||
|
|
||||||
let cipher = crypto.createCipheriv(TokenCache.CipherAlgorithm, encryptionParams.key, encryptionParams.initializationVector);
|
let cipher = crypto.createCipheriv(TokenCache.CipherAlgorithm, encryptionParams.key, encryptionParams.initializationVector);
|
||||||
let cacheCipher = cipher.update(cacheJson, 'binary', 'hex');
|
let cacheCipher = cipher.update(cacheJson, 'utf8', 'hex');
|
||||||
cacheCipher += cipher.final('hex');
|
cacheCipher += cipher.final('hex');
|
||||||
|
|
||||||
fs.writeFileSync(self._cacheSerializationPath, cacheCipher, TokenCache.FsOptions);
|
fs.writeFileSync(self._cacheSerializationPath, cacheCipher, TokenCache.FsOptions);
|
||||||
|
|||||||
@@ -212,8 +212,8 @@ export class ApiWrapper {
|
|||||||
return sqlops.accounts.getAllAccounts();
|
return sqlops.accounts.getAllAccounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSecurityToken(account: sqlops.Account): Thenable<{}> {
|
public getSecurityToken(account: sqlops.Account, resource: sqlops.AzureResource): Thenable<{}> {
|
||||||
return sqlops.accounts.getSecurityToken(account);
|
return sqlops.accounts.getSecurityToken(account, resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly onDidChangeAccounts = sqlops.accounts.onDidChangeAccounts;
|
public readonly onDidChangeAccounts = sqlops.accounts.onDidChangeAccounts;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import { window, QuickPickItem } from 'vscode';
|
import { window, QuickPickItem } from 'vscode';
|
||||||
import { IConnectionProfile } from 'sqlops';
|
import * as sqlops from 'sqlops';
|
||||||
import { generateGuid } from './utils';
|
import { generateGuid } from './utils';
|
||||||
import { ApiWrapper } from '../apiWrapper';
|
import { ApiWrapper } from '../apiWrapper';
|
||||||
import { TreeNode } from '../treeNodes';
|
import { TreeNode } from '../treeNodes';
|
||||||
@@ -30,7 +30,7 @@ export function registerAzureResourceCommands(apiWrapper: ApiWrapper, tree: Azur
|
|||||||
|
|
||||||
let subscriptions = await accountNode.getCachedSubscriptions();
|
let subscriptions = await accountNode.getCachedSubscriptions();
|
||||||
if (!subscriptions || subscriptions.length === 0) {
|
if (!subscriptions || subscriptions.length === 0) {
|
||||||
const credentials = await servicePool.credentialService.getCredentials(accountNode.account);
|
const credentials = await servicePool.credentialService.getCredentials(accountNode.account, sqlops.AzureResource.ResourceManagement);
|
||||||
subscriptions = await servicePool.subscriptionService.getSubscriptions(accountNode.account, credentials);
|
subscriptions = await servicePool.subscriptionService.getSubscriptions(accountNode.account, credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ export function registerAzureResourceCommands(apiWrapper: ApiWrapper, tree: Azur
|
|||||||
});
|
});
|
||||||
|
|
||||||
apiWrapper.registerCommand('azureresource.connectsqldb', async (node?: TreeNode) => {
|
apiWrapper.registerCommand('azureresource.connectsqldb', async (node?: TreeNode) => {
|
||||||
let connectionProfile: IConnectionProfile = {
|
let connectionProfile: sqlops.IConnectionProfile = {
|
||||||
id: generateGuid(),
|
id: generateGuid(),
|
||||||
connectionName: undefined,
|
connectionName: undefined,
|
||||||
serverName: undefined,
|
serverName: undefined,
|
||||||
|
|||||||
@@ -6,29 +6,29 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import { ServiceClientCredentials } from 'ms-rest';
|
import { ServiceClientCredentials } from 'ms-rest';
|
||||||
import { Account, DidChangeAccountsParams } from 'sqlops';
|
import * as sqlops from 'sqlops';
|
||||||
import { Event } from 'vscode';
|
import { Event } from 'vscode';
|
||||||
|
|
||||||
import { AzureResourceSubscription, AzureResourceDatabaseServer, AzureResourceDatabase } from './models';
|
import { AzureResourceSubscription, AzureResourceDatabaseServer, AzureResourceDatabase } from './models';
|
||||||
|
|
||||||
export interface IAzureResourceAccountService {
|
export interface IAzureResourceAccountService {
|
||||||
getAccounts(): Promise<Account[]>;
|
getAccounts(): Promise<sqlops.Account[]>;
|
||||||
|
|
||||||
readonly onDidChangeAccounts: Event<DidChangeAccountsParams>;
|
readonly onDidChangeAccounts: Event<sqlops.DidChangeAccountsParams>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAzureResourceCredentialService {
|
export interface IAzureResourceCredentialService {
|
||||||
getCredentials(account: Account): Promise<ServiceClientCredentials[]>;
|
getCredentials(account: sqlops.Account, resource: sqlops.AzureResource): Promise<ServiceClientCredentials[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAzureResourceSubscriptionService {
|
export interface IAzureResourceSubscriptionService {
|
||||||
getSubscriptions(account: Account, credentials: ServiceClientCredentials[]): Promise<AzureResourceSubscription[]>;
|
getSubscriptions(account: sqlops.Account, credentials: ServiceClientCredentials[]): Promise<AzureResourceSubscription[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAzureResourceSubscriptionFilterService {
|
export interface IAzureResourceSubscriptionFilterService {
|
||||||
getSelectedSubscriptions(account: Account): Promise<AzureResourceSubscription[]>;
|
getSelectedSubscriptions(account: sqlops.Account): Promise<AzureResourceSubscription[]>;
|
||||||
|
|
||||||
saveSelectedSubscriptions(account: Account, selectedSubscriptions: AzureResourceSubscription[]): Promise<void>;
|
saveSelectedSubscriptions(account: sqlops.Account, selectedSubscriptions: AzureResourceSubscription[]): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAzureResourceDatabaseServerService {
|
export interface IAzureResourceDatabaseServerService {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import { Account } from 'sqlops';
|
import * as sqlops from 'sqlops';
|
||||||
import { TokenCredentials, ServiceClientCredentials } from 'ms-rest';
|
import { TokenCredentials, ServiceClientCredentials } from 'ms-rest';
|
||||||
import { ApiWrapper } from '../../apiWrapper';
|
import { ApiWrapper } from '../../apiWrapper';
|
||||||
import * as nls from 'vscode-nls';
|
import * as nls from 'vscode-nls';
|
||||||
@@ -21,10 +21,10 @@ export class AzureResourceCredentialService implements IAzureResourceCredentialS
|
|||||||
this._apiWrapper = apiWrapper;
|
this._apiWrapper = apiWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getCredentials(account: Account): Promise<ServiceClientCredentials[]> {
|
public async getCredentials(account: sqlops.Account, resource: sqlops.AzureResource): Promise<ServiceClientCredentials[]> {
|
||||||
try {
|
try {
|
||||||
let credentials: TokenCredentials[] = [];
|
let credentials: TokenCredentials[] = [];
|
||||||
let tokens = await this._apiWrapper.getSecurityToken(account);
|
let tokens = await this._apiWrapper.getSecurityToken(account, resource);
|
||||||
|
|
||||||
for (let tenant of account.properties.tenants) {
|
for (let tenant of account.properties.tenants) {
|
||||||
let token = tokens[tenant.id].token;
|
let token = tokens[tenant.id].token;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import { Account } from 'sqlops';
|
import * as sqlops from 'sqlops';
|
||||||
import { ServiceClientCredentials } from 'ms-rest';
|
import { ServiceClientCredentials } from 'ms-rest';
|
||||||
import { TreeNode } from '../../treeNodes';
|
import { TreeNode } from '../../treeNodes';
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ export abstract class AzureResourceTreeNodeBase extends TreeNode {
|
|||||||
|
|
||||||
export abstract class AzureResourceContainerTreeNodeBase extends AzureResourceTreeNodeBase {
|
export abstract class AzureResourceContainerTreeNodeBase extends AzureResourceTreeNodeBase {
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly account: Account,
|
public readonly account: sqlops.Account,
|
||||||
treeChangeHandler: IAzureResourceTreeChangeHandler,
|
treeChangeHandler: IAzureResourceTreeChangeHandler,
|
||||||
parent: TreeNode
|
parent: TreeNode
|
||||||
) {
|
) {
|
||||||
@@ -45,7 +45,7 @@ export abstract class AzureResourceContainerTreeNodeBase extends AzureResourceTr
|
|||||||
|
|
||||||
protected async getCredentials(): Promise<ServiceClientCredentials[]> {
|
protected async getCredentials(): Promise<ServiceClientCredentials[]> {
|
||||||
try {
|
try {
|
||||||
return await this.servicePool.credentialService.getCredentials(this.account);
|
return await this.servicePool.credentialService.getCredentials(this.account, sqlops.AzureResource.ResourceManagement);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AzureResourceCredentialError) {
|
if (error instanceof AzureResourceCredentialError) {
|
||||||
this.servicePool.contextService.showErrorMessage(error.message);
|
this.servicePool.contextService.showErrorMessage(error.message);
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ describe('AzureResourceAccountTreeNode.info', function(): void {
|
|||||||
mockServicePool.subscriptionService = mockSubscriptionService.object;
|
mockServicePool.subscriptionService = mockSubscriptionService.object;
|
||||||
mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object;
|
mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object;
|
||||||
|
|
||||||
mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials));
|
mockCredentialService.setup((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockCredentials));
|
||||||
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
|
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
|
||||||
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache.subscriptions[mockAccount.key.accountId] = mockSubscriptions);
|
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache.subscriptions[mockAccount.key.accountId] = mockSubscriptions);
|
||||||
});
|
});
|
||||||
@@ -164,7 +164,7 @@ describe('AzureResourceAccountTreeNode.getChildren', function(): void {
|
|||||||
mockServicePool.subscriptionService = mockSubscriptionService.object;
|
mockServicePool.subscriptionService = mockSubscriptionService.object;
|
||||||
mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object;
|
mockServicePool.subscriptionFilterService = mockSubscriptionFilterService.object;
|
||||||
|
|
||||||
mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials));
|
mockCredentialService.setup((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockCredentials));
|
||||||
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
|
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockSubscriptionCache);
|
||||||
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache.subscriptions[mockAccount.key.accountId] = mockSubscriptions);
|
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockSubscriptionCache.subscriptions[mockAccount.key.accountId] = mockSubscriptions);
|
||||||
});
|
});
|
||||||
@@ -177,7 +177,7 @@ describe('AzureResourceAccountTreeNode.getChildren', function(): void {
|
|||||||
|
|
||||||
const children = await accountTreeNode.getChildren();
|
const children = await accountTreeNode.getChildren();
|
||||||
|
|
||||||
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once());
|
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once());
|
||||||
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.once());
|
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.once());
|
||||||
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
|
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
|
||||||
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||||
@@ -213,7 +213,7 @@ describe('AzureResourceAccountTreeNode.getChildren', function(): void {
|
|||||||
await accountTreeNode.getChildren();
|
await accountTreeNode.getChildren();
|
||||||
const children = await accountTreeNode.getChildren();
|
const children = await accountTreeNode.getChildren();
|
||||||
|
|
||||||
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.exactly(1));
|
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.exactly(1));
|
||||||
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.exactly(1));
|
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.exactly(1));
|
||||||
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(2));
|
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(2));
|
||||||
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(1));
|
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(1));
|
||||||
@@ -267,7 +267,7 @@ describe('AzureResourceAccountTreeNode.getChildren', function(): void {
|
|||||||
|
|
||||||
const children = await accountTreeNode.getChildren();
|
const children = await accountTreeNode.getChildren();
|
||||||
|
|
||||||
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once());
|
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once());
|
||||||
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.once());
|
mockSubscriptionService.verify((o) => o.getSubscriptions(mockAccount, mockCredentials), TypeMoq.Times.once());
|
||||||
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never());
|
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never());
|
||||||
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never());
|
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never());
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ describe('AzureResourceDatabaseContainerTreeNode.getChildren', function(): void
|
|||||||
mockServicePool.credentialService = mockCredentialService.object;
|
mockServicePool.credentialService = mockCredentialService.object;
|
||||||
mockServicePool.databaseService = mockDatabaseService.object;
|
mockServicePool.databaseService = mockDatabaseService.object;
|
||||||
|
|
||||||
mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials));
|
mockCredentialService.setup((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockCredentials));
|
||||||
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockDatabaseContainerCache);
|
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockDatabaseContainerCache);
|
||||||
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockDatabaseContainerCache.databases[mockSubscription.id] = mockDatabases);
|
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockDatabaseContainerCache.databases[mockSubscription.id] = mockDatabases);
|
||||||
});
|
});
|
||||||
@@ -130,7 +130,7 @@ describe('AzureResourceDatabaseContainerTreeNode.getChildren', function(): void
|
|||||||
|
|
||||||
const children = await databaseContainerTreeNode.getChildren();
|
const children = await databaseContainerTreeNode.getChildren();
|
||||||
|
|
||||||
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once());
|
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once());
|
||||||
mockDatabaseService.verify((o) => o.getDatabases(mockSubscription, mockCredentials), TypeMoq.Times.once());
|
mockDatabaseService.verify((o) => o.getDatabases(mockSubscription, mockCredentials), TypeMoq.Times.once());
|
||||||
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
|
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
|
||||||
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||||
@@ -160,7 +160,7 @@ describe('AzureResourceDatabaseContainerTreeNode.getChildren', function(): void
|
|||||||
await databaseContainerTreeNode.getChildren();
|
await databaseContainerTreeNode.getChildren();
|
||||||
const children = await databaseContainerTreeNode.getChildren();
|
const children = await databaseContainerTreeNode.getChildren();
|
||||||
|
|
||||||
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.exactly(1));
|
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.exactly(1));
|
||||||
mockDatabaseService.verify((o) => o.getDatabases(mockSubscription, mockCredentials), TypeMoq.Times.exactly(1));
|
mockDatabaseService.verify((o) => o.getDatabases(mockSubscription, mockCredentials), TypeMoq.Times.exactly(1));
|
||||||
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(2));
|
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(2));
|
||||||
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(1));
|
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(1));
|
||||||
@@ -193,7 +193,7 @@ describe('AzureResourceDatabaseContainerTreeNode.getChildren', function(): void
|
|||||||
const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
|
const databaseContainerTreeNode = new AzureResourceDatabaseContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
|
||||||
const children = await databaseContainerTreeNode.getChildren();
|
const children = await databaseContainerTreeNode.getChildren();
|
||||||
|
|
||||||
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once());
|
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once());
|
||||||
mockDatabaseService.verify((o) => o.getDatabases(mockSubscription, mockCredentials), TypeMoq.Times.once());
|
mockDatabaseService.verify((o) => o.getDatabases(mockSubscription, mockCredentials), TypeMoq.Times.once());
|
||||||
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never());
|
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never());
|
||||||
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never());
|
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never());
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ describe('AzureResourceDatabaseServerContainerTreeNode.getChildren', function():
|
|||||||
mockServicePool.credentialService = mockCredentialService.object;
|
mockServicePool.credentialService = mockCredentialService.object;
|
||||||
mockServicePool.databaseServerService = mockDatabaseServerService.object;
|
mockServicePool.databaseServerService = mockDatabaseServerService.object;
|
||||||
|
|
||||||
mockCredentialService.setup((o) => o.getCredentials(mockAccount)).returns(() => Promise.resolve(mockCredentials));
|
mockCredentialService.setup((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement)).returns(() => Promise.resolve(mockCredentials));
|
||||||
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockDatabaseServerContainerCache);
|
mockCacheService.setup((o) => o.get(TypeMoq.It.isAnyString())).returns(() => mockDatabaseServerContainerCache);
|
||||||
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockDatabaseServerContainerCache.databaseServers[mockSubscription.id] = mockDatabaseServers);
|
mockCacheService.setup((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns(() => mockDatabaseServerContainerCache.databaseServers[mockSubscription.id] = mockDatabaseServers);
|
||||||
});
|
});
|
||||||
@@ -130,7 +130,7 @@ describe('AzureResourceDatabaseServerContainerTreeNode.getChildren', function():
|
|||||||
|
|
||||||
const children = await databaseServerContainerTreeNode.getChildren();
|
const children = await databaseServerContainerTreeNode.getChildren();
|
||||||
|
|
||||||
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once());
|
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once());
|
||||||
mockDatabaseServerService.verify((o) => o.getDatabaseServers(mockSubscription, mockCredentials), TypeMoq.Times.once());
|
mockDatabaseServerService.verify((o) => o.getDatabaseServers(mockSubscription, mockCredentials), TypeMoq.Times.once());
|
||||||
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
|
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.once());
|
||||||
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||||
@@ -160,7 +160,7 @@ describe('AzureResourceDatabaseServerContainerTreeNode.getChildren', function():
|
|||||||
await databaseServerContainerTreeNode.getChildren();
|
await databaseServerContainerTreeNode.getChildren();
|
||||||
const children = await databaseServerContainerTreeNode.getChildren();
|
const children = await databaseServerContainerTreeNode.getChildren();
|
||||||
|
|
||||||
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.exactly(1));
|
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.exactly(1));
|
||||||
mockDatabaseServerService.verify((o) => o.getDatabaseServers(mockSubscription, mockCredentials), TypeMoq.Times.exactly(1));
|
mockDatabaseServerService.verify((o) => o.getDatabaseServers(mockSubscription, mockCredentials), TypeMoq.Times.exactly(1));
|
||||||
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(2));
|
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(2));
|
||||||
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(1));
|
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(1));
|
||||||
@@ -193,7 +193,7 @@ describe('AzureResourceDatabaseServerContainerTreeNode.getChildren', function():
|
|||||||
const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
|
const databaseServerContainerTreeNode = new AzureResourceDatabaseServerContainerTreeNode(mockSubscription, mockAccount, mockTreeChangeHandler.object, undefined);
|
||||||
const children = await databaseServerContainerTreeNode.getChildren();
|
const children = await databaseServerContainerTreeNode.getChildren();
|
||||||
|
|
||||||
mockCredentialService.verify((o) => o.getCredentials(mockAccount), TypeMoq.Times.once());
|
mockCredentialService.verify((o) => o.getCredentials(mockAccount, sqlops.AzureResource.ResourceManagement), TypeMoq.Times.once());
|
||||||
mockDatabaseServerService.verify((o) => o.getDatabaseServers(mockSubscription, mockCredentials), TypeMoq.Times.once());
|
mockDatabaseServerService.verify((o) => o.getDatabaseServers(mockSubscription, mockCredentials), TypeMoq.Times.once());
|
||||||
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never());
|
mockCacheService.verify((o) => o.get(TypeMoq.It.isAnyString()), TypeMoq.Times.never());
|
||||||
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never());
|
mockCacheService.verify((o) => o.update(TypeMoq.It.isAnyString(), TypeMoq.It.isAny()), TypeMoq.Times.never());
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "import",
|
"name": "import",
|
||||||
"displayName": "SQL Server Import",
|
"displayName": "SQL Server Import",
|
||||||
"description": "SQL Server Import for Azure Data Studio supports importing CSV or JSON files into SQL Server.",
|
"description": "SQL Server Import for Azure Data Studio supports importing CSV or JSON files into SQL Server.",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"publisher": "Microsoft",
|
"publisher": "Microsoft",
|
||||||
"preview": true,
|
"preview": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -33,6 +33,15 @@
|
|||||||
"light": "./images/light_icon.svg",
|
"light": "./images/light_icon.svg",
|
||||||
"dark": "./images/dark_icon.svg"
|
"dark": "./images/dark_icon.svg"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "dacFx.start",
|
||||||
|
"title": "Data-tier Application Wizard",
|
||||||
|
"category": "Data-tier Application",
|
||||||
|
"icon": {
|
||||||
|
"light": "./images/light_icon.svg",
|
||||||
|
"dark": "./images/dark_icon.svg"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"keybindings": [
|
"keybindings": [
|
||||||
@@ -48,6 +57,11 @@
|
|||||||
"command": "flatFileImport.start",
|
"command": "flatFileImport.start",
|
||||||
"when": "connectionProvider == MSSQL && nodeType && nodeType == Database",
|
"when": "connectionProvider == MSSQL && nodeType && nodeType == Database",
|
||||||
"group": "import"
|
"group": "import"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "dacFx.start",
|
||||||
|
"when": "connectionProvider == MSSQL && nodeType && nodeType == Database",
|
||||||
|
"group": "export"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { FlatFileWizard } from '../wizard/flatFileWizard';
|
|||||||
import { ServiceClient } from '../services/serviceClient';
|
import { ServiceClient } from '../services/serviceClient';
|
||||||
import { ApiType, managerInstance } from '../services/serviceApiManager';
|
import { ApiType, managerInstance } from '../services/serviceApiManager';
|
||||||
import { FlatFileProvider } from '../services/contracts';
|
import { FlatFileProvider } from '../services/contracts';
|
||||||
|
import { DataTierApplicationWizard } from '../wizard/dataTierApplicationWizard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main controller class that initializes the extension
|
* The main controller class that initializes the extension
|
||||||
@@ -35,10 +36,15 @@ export default class MainController extends ControllerBase {
|
|||||||
this.initializeFlatFileProvider(provider);
|
this.initializeFlatFileProvider(provider);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.initializeDacFxWizard();
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeFlatFileProvider(provider: FlatFileProvider) {
|
private initializeFlatFileProvider(provider: FlatFileProvider) {
|
||||||
sqlops.tasks.registerTask('flatFileImport.start', (profile: sqlops.IConnectionProfile, ...args: any[]) => new FlatFileWizard(provider).start(profile, args));
|
sqlops.tasks.registerTask('flatFileImport.start', (profile: sqlops.IConnectionProfile, ...args: any[]) => new FlatFileWizard(provider).start(profile, args));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private initializeDacFxWizard() {
|
||||||
|
sqlops.tasks.registerTask('dacFx.start', (profile: sqlops.IConnectionProfile, ...args: any[]) => new DataTierApplicationWizard().start(profile, args));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
139
extensions/import/src/wizard/api/basePage.ts
Normal file
139
extensions/import/src/wizard/api/basePage.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 { BaseDataModel } from './models';
|
||||||
|
|
||||||
|
export abstract class BasePage {
|
||||||
|
|
||||||
|
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
|
||||||
|
protected readonly model: BaseDataModel;
|
||||||
|
protected readonly view: sqlops.ModelView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method constructs all the elements of the page.
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
public async abstract start(): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called when the user is entering the page.
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
public async abstract onPageEnter(): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called when the user is leaving the page.
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async onPageLeave(): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override this method to cleanup what you don't need cached in the page.
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
public async cleanup(): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a navigation validator.
|
||||||
|
* This will be called right before onPageEnter().
|
||||||
|
*/
|
||||||
|
public abstract setupNavigationValidator();
|
||||||
|
|
||||||
|
protected async getServerValues(): Promise<{ connection, displayName, name }[]> {
|
||||||
|
let cons = await sqlops.connection.getActiveConnections();
|
||||||
|
// This user has no active connections ABORT MISSION
|
||||||
|
if (!cons || cons.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = -1;
|
||||||
|
let idx = -1;
|
||||||
|
|
||||||
|
|
||||||
|
let values = cons.map(c => {
|
||||||
|
// Handle the code to remember what the user's choice was from before
|
||||||
|
count++;
|
||||||
|
if (idx === -1) {
|
||||||
|
if (this.model.server && c.connectionId === this.model.server.connectionId) {
|
||||||
|
idx = count;
|
||||||
|
} else if (this.model.serverId && c.connectionId === this.model.serverId) {
|
||||||
|
idx = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let db = c.options.databaseDisplayName;
|
||||||
|
let usr = c.options.user;
|
||||||
|
let srv = c.options.server;
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
db = '<default>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!usr) {
|
||||||
|
usr = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalName = `${srv}, ${db} (${usr})`;
|
||||||
|
return {
|
||||||
|
connection: c,
|
||||||
|
displayName: finalName,
|
||||||
|
name: c.connectionId
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (idx >= 0) {
|
||||||
|
let tmp = values[0];
|
||||||
|
values[0] = values[idx];
|
||||||
|
values[idx] = tmp;
|
||||||
|
} else {
|
||||||
|
this.deleteServerValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getDatabaseValues(): Promise<{ displayName, name }[]> {
|
||||||
|
let idx = -1;
|
||||||
|
let count = -1;
|
||||||
|
let values = (await sqlops.connection.listDatabases(this.model.server.connectionId)).map(db => {
|
||||||
|
count++;
|
||||||
|
if (this.model.database && db === this.model.database) {
|
||||||
|
idx = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayName: db,
|
||||||
|
name: db
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (idx >= 0) {
|
||||||
|
let tmp = values[0];
|
||||||
|
values[0] = values[idx];
|
||||||
|
values[idx] = tmp;
|
||||||
|
} else {
|
||||||
|
this.deleteDatabaseValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected deleteServerValues() {
|
||||||
|
delete this.model.server;
|
||||||
|
delete this.model.serverId;
|
||||||
|
delete this.model.database;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected deleteDatabaseValues() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
158
extensions/import/src/wizard/api/dacFxConfigPage.ts
Normal file
158
extensions/import/src/wizard/api/dacFxConfigPage.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 nls from 'vscode-nls';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
|
||||||
|
import { DacFxDataModel } from './models';
|
||||||
|
import { BasePage } from './basePage';
|
||||||
|
|
||||||
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
|
export abstract class DacFxConfigPage extends BasePage {
|
||||||
|
|
||||||
|
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
|
||||||
|
protected readonly instance: DataTierApplicationWizard;
|
||||||
|
protected readonly model: DacFxDataModel;
|
||||||
|
protected readonly view: sqlops.ModelView;
|
||||||
|
protected serverDropdown: sqlops.DropDownComponent;
|
||||||
|
protected databaseTextBox: sqlops.InputBoxComponent;
|
||||||
|
protected databaseDropdown: sqlops.DropDownComponent;
|
||||||
|
protected databaseLoader: sqlops.LoadingComponent;
|
||||||
|
protected fileTextBox: sqlops.InputBoxComponent;
|
||||||
|
protected fileButton: sqlops.ButtonComponent;
|
||||||
|
protected fileExtension: string;
|
||||||
|
|
||||||
|
protected constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) {
|
||||||
|
super();
|
||||||
|
this.instance = instance;
|
||||||
|
this.wizardPage = wizardPage;
|
||||||
|
this.model = model;
|
||||||
|
this.view = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setupNavigationValidator() {
|
||||||
|
this.instance.registerNavigationValidator(() => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async createServerDropdown(isTargetServer: boolean): Promise<sqlops.FormComponent> {
|
||||||
|
this.serverDropdown = this.view.modelBuilder.dropDown().withProperties({
|
||||||
|
required: true
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
// Handle server changes
|
||||||
|
this.serverDropdown.onValueChanged(async () => {
|
||||||
|
this.model.server = (this.serverDropdown.value as ConnectionDropdownValue).connection;
|
||||||
|
this.model.serverName = (this.serverDropdown.value as ConnectionDropdownValue).displayName;
|
||||||
|
await this.populateDatabaseDropdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
let targetServerTitle = localize('dacFx.targetServerDropdownTitle', 'Target Server');
|
||||||
|
let sourceServerTitle = localize('dacFx.sourceServerDropdownTitle', 'Source Server');
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: this.serverDropdown,
|
||||||
|
title: isTargetServer ? targetServerTitle : sourceServerTitle
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async populateServerDropdown(): Promise<boolean> {
|
||||||
|
let values = await this.getServerValues();
|
||||||
|
if (values === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.model.server = values[0].connection;
|
||||||
|
this.model.serverName = values[0].displayName;
|
||||||
|
|
||||||
|
this.serverDropdown.updateProperties({
|
||||||
|
values: values
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async createDatabaseTextBox(): Promise<sqlops.FormComponent> {
|
||||||
|
this.databaseTextBox = this.view.modelBuilder.inputBox().withProperties({
|
||||||
|
required: true
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.databaseTextBox.onTextChanged(async () => {
|
||||||
|
this.model.database = this.databaseTextBox.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: this.databaseTextBox,
|
||||||
|
title: localize('dacFx.databaseNameTextBox', 'Target Database')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async createDatabaseDropdown(): Promise<sqlops.FormComponent> {
|
||||||
|
this.databaseDropdown = this.view.modelBuilder.dropDown().withProperties({
|
||||||
|
required: true
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
// Handle database changes
|
||||||
|
this.databaseDropdown.onValueChanged(async () => {
|
||||||
|
this.model.database = (<sqlops.CategoryValue>this.databaseDropdown.value).name;
|
||||||
|
this.fileTextBox.value = this.generateFilePath();
|
||||||
|
this.model.filePath = this.fileTextBox.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.databaseLoader = this.view.modelBuilder.loadingComponent().withItem(this.databaseDropdown).component();
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: this.databaseLoader,
|
||||||
|
title: localize('dacFx.sourceDatabaseDropdownTitle', 'Source Database')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async populateDatabaseDropdown(): Promise<boolean> {
|
||||||
|
this.databaseLoader.loading = true;
|
||||||
|
this.databaseDropdown.updateProperties({ values: [] });
|
||||||
|
|
||||||
|
if (!this.model.server) {
|
||||||
|
this.databaseLoader.loading = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let values = await this.getDatabaseValues();
|
||||||
|
this.model.database = values[0].name;
|
||||||
|
this.model.filePath = this.generateFilePath();
|
||||||
|
this.fileTextBox.value = this.model.filePath;
|
||||||
|
|
||||||
|
this.databaseDropdown.updateProperties({
|
||||||
|
values: values
|
||||||
|
});
|
||||||
|
this.databaseLoader.loading = false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async createFileBrowserParts() {
|
||||||
|
this.fileTextBox = this.view.modelBuilder.inputBox().withProperties({
|
||||||
|
required: true
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.fileButton = this.view.modelBuilder.button().withProperties({
|
||||||
|
label: '•••',
|
||||||
|
}).component();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected generateFilePath(): string {
|
||||||
|
let now = new Date();
|
||||||
|
let datetime = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate() + '-' + now.getHours() + '-' + now.getMinutes();
|
||||||
|
return path.join(os.homedir(), this.model.database + '-' + datetime + this.fileExtension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConnectionDropdownValue extends sqlops.CategoryValue {
|
||||||
|
connection: sqlops.connection.Connection;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -8,8 +8,9 @@ import { ImportDataModel } from './models';
|
|||||||
import * as sqlops from 'sqlops';
|
import * as sqlops from 'sqlops';
|
||||||
import { FlatFileProvider } from '../../services/contracts';
|
import { FlatFileProvider } from '../../services/contracts';
|
||||||
import { FlatFileWizard } from '../flatFileWizard';
|
import { FlatFileWizard } from '../flatFileWizard';
|
||||||
|
import { BasePage } from './basePage';
|
||||||
|
|
||||||
export abstract class ImportPage {
|
export abstract class ImportPage extends BasePage {
|
||||||
|
|
||||||
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
|
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
|
||||||
protected readonly instance: FlatFileWizard;
|
protected readonly instance: FlatFileWizard;
|
||||||
@@ -18,42 +19,11 @@ export abstract class ImportPage {
|
|||||||
protected readonly provider: FlatFileProvider;
|
protected readonly provider: FlatFileProvider;
|
||||||
|
|
||||||
protected constructor(instance: FlatFileWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: ImportDataModel, view: sqlops.ModelView, provider: FlatFileProvider) {
|
protected constructor(instance: FlatFileWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: ImportDataModel, view: sqlops.ModelView, provider: FlatFileProvider) {
|
||||||
|
super();
|
||||||
this.instance = instance;
|
this.instance = instance;
|
||||||
this.wizardPage = wizardPage;
|
this.wizardPage = wizardPage;
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.view = view;
|
this.view = view;
|
||||||
this.provider = provider;
|
this.provider = provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This method constructs all the elements of the page.
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
public async abstract start(): Promise<boolean>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is called when the user is entering the page.
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
public async abstract onPageEnter(): Promise<boolean>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is called when the user is leaving the page.
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
public async abstract onPageLeave(): Promise<boolean>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up a navigation validator.
|
|
||||||
* This will be called right before onPageEnter().
|
|
||||||
*/
|
|
||||||
public abstract setupNavigationValidator();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Override this method to cleanup what you don't need cached in the page.
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
public async cleanup(): Promise<boolean> {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,19 @@
|
|||||||
|
|
||||||
import * as sqlops from 'sqlops';
|
import * as sqlops from 'sqlops';
|
||||||
|
|
||||||
|
export interface BaseDataModel {
|
||||||
|
server: sqlops.connection.Connection;
|
||||||
|
serverId: string;
|
||||||
|
database: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main data model that communicates between the pages.
|
* The main data model that communicates between the pages.
|
||||||
*/
|
*/
|
||||||
export interface ImportDataModel {
|
export interface ImportDataModel extends BaseDataModel {
|
||||||
ownerUri: string;
|
ownerUri: string;
|
||||||
proseColumns: ColumnMetadata[];
|
proseColumns: ColumnMetadata[];
|
||||||
proseDataPreview: string[][];
|
proseDataPreview: string[][];
|
||||||
server: sqlops.connection.Connection;
|
|
||||||
serverId: string;
|
|
||||||
database: string;
|
database: string;
|
||||||
table: string;
|
table: string;
|
||||||
schema: string;
|
schema: string;
|
||||||
@@ -31,3 +35,14 @@ export interface ColumnMetadata {
|
|||||||
primaryKey: boolean;
|
primaryKey: boolean;
|
||||||
nullable: boolean;
|
nullable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data model to communicate between DacFx pages
|
||||||
|
*/
|
||||||
|
export interface DacFxDataModel extends BaseDataModel {
|
||||||
|
serverName: string;
|
||||||
|
serverId: string;
|
||||||
|
filePath: string;
|
||||||
|
version: string;
|
||||||
|
upgradeExisting: boolean;
|
||||||
|
}
|
||||||
|
|||||||
253
extensions/import/src/wizard/dataTierApplicationWizard.ts
Normal file
253
extensions/import/src/wizard/dataTierApplicationWizard.ts
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 nls from 'vscode-nls';
|
||||||
|
import * as sqlops from 'sqlops';
|
||||||
|
import { SelectOperationPage } from './pages/selectOperationpage';
|
||||||
|
import { DeployConfigPage } from './pages/deployConfigPage';
|
||||||
|
import { DacFxSummaryPage } from './pages/dacFxSummaryPage';
|
||||||
|
import { ExportConfigPage } from './pages/exportConfigPage';
|
||||||
|
import { ExtractConfigPage } from './pages/extractConfigPage';
|
||||||
|
import { ImportConfigPage } from './pages/importConfigPage';
|
||||||
|
import { DacFxDataModel } from './api/models';
|
||||||
|
import { BasePage } from './api/basePage';
|
||||||
|
|
||||||
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
|
class Page {
|
||||||
|
wizardPage: sqlops.window.modelviewdialog.WizardPage;
|
||||||
|
dacFxPage: BasePage;
|
||||||
|
|
||||||
|
constructor(wizardPage: sqlops.window.modelviewdialog.WizardPage) {
|
||||||
|
this.wizardPage = wizardPage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Operation {
|
||||||
|
deploy,
|
||||||
|
extract,
|
||||||
|
import,
|
||||||
|
export
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DataTierApplicationWizard {
|
||||||
|
public wizard: sqlops.window.modelviewdialog.Wizard;
|
||||||
|
private connection: sqlops.connection.Connection;
|
||||||
|
private model: DacFxDataModel;
|
||||||
|
public pages: Map<string, Page> = new Map<string, Page>();
|
||||||
|
public selectedOperation: Operation;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start(p: any, ...args: any[]) {
|
||||||
|
this.model = <DacFxDataModel>{};
|
||||||
|
|
||||||
|
let profile = p ? <sqlops.IConnectionProfile>p.connectionProfile : undefined;
|
||||||
|
if (profile) {
|
||||||
|
this.model.serverId = profile.id;
|
||||||
|
this.model.database = profile.databaseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connection = await sqlops.connection.getCurrentConnection();
|
||||||
|
if (!this.connection) {
|
||||||
|
this.connection = await sqlops.connection.openConnectionDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.wizard = sqlops.window.modelviewdialog.createWizard('Data-tier Application Wizard');
|
||||||
|
let selectOperationWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.selectOperationPageName', 'Select an Operation'));
|
||||||
|
let deployConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.deployConfigPageName', 'Select Deploy Dacpac Settings'));
|
||||||
|
let summaryWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.summaryPageName', 'Summary'));
|
||||||
|
let extractConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.extractConfigPageName', 'Select Extract Dacpac Settings'));
|
||||||
|
let importConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.importConfigPageName', 'Select Import Bacpac Settings'));
|
||||||
|
let exportConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.exportConfigPageName', 'Select Export Bacpac Settings'));
|
||||||
|
|
||||||
|
this.pages.set('selectOperation', new Page(selectOperationWizardPage));
|
||||||
|
this.pages.set('deployConfig', new Page(deployConfigWizardPage));
|
||||||
|
this.pages.set('extractConfig', new Page(extractConfigWizardPage));
|
||||||
|
this.pages.set('importConfig', new Page(importConfigWizardPage));
|
||||||
|
this.pages.set('exportConfig', new Page(exportConfigWizardPage));
|
||||||
|
this.pages.set('summary', new Page(summaryWizardPage));
|
||||||
|
|
||||||
|
selectOperationWizardPage.registerContent(async (view) => {
|
||||||
|
let selectOperationDacFxPage = new SelectOperationPage(this, selectOperationWizardPage, this.model, view);
|
||||||
|
this.pages.get('selectOperation').dacFxPage = selectOperationDacFxPage;
|
||||||
|
await selectOperationDacFxPage.start().then(() => {
|
||||||
|
selectOperationDacFxPage.setupNavigationValidator();
|
||||||
|
selectOperationDacFxPage.onPageEnter();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
deployConfigWizardPage.registerContent(async (view) => {
|
||||||
|
let deployConfigDacFxPage = new DeployConfigPage(this, deployConfigWizardPage, this.model, view);
|
||||||
|
this.pages.get('deployConfig').dacFxPage = deployConfigDacFxPage;
|
||||||
|
await deployConfigDacFxPage.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
extractConfigWizardPage.registerContent(async (view) => {
|
||||||
|
let extractConfigDacFxPage = new ExtractConfigPage(this, extractConfigWizardPage, this.model, view);
|
||||||
|
this.pages.get('extractConfig').dacFxPage = extractConfigDacFxPage;
|
||||||
|
await extractConfigDacFxPage.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
importConfigWizardPage.registerContent(async (view) => {
|
||||||
|
let importConfigDacFxPage = new ImportConfigPage(this, importConfigWizardPage, this.model, view);
|
||||||
|
this.pages.get('importConfig').dacFxPage = importConfigDacFxPage;
|
||||||
|
await importConfigDacFxPage.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
exportConfigWizardPage.registerContent(async (view) => {
|
||||||
|
let exportConfigDacFxPage = new ExportConfigPage(this, exportConfigWizardPage, this.model, view);
|
||||||
|
this.pages.get('exportConfig').dacFxPage = exportConfigDacFxPage;
|
||||||
|
await exportConfigDacFxPage.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
summaryWizardPage.registerContent(async (view) => {
|
||||||
|
let summaryDacFxPage = new DacFxSummaryPage(this, summaryWizardPage, this.model, view);
|
||||||
|
this.pages.get('summary').dacFxPage = summaryDacFxPage;
|
||||||
|
await summaryDacFxPage.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.wizard.onPageChanged(async (event) => {
|
||||||
|
let idx = event.newPage;
|
||||||
|
let page: Page;
|
||||||
|
|
||||||
|
if (idx === 1) {
|
||||||
|
switch (this.selectedOperation) {
|
||||||
|
case Operation.deploy: {
|
||||||
|
page = this.pages.get('deployConfig');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Operation.extract: {
|
||||||
|
page = this.pages.get('extractConfig');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Operation.import: {
|
||||||
|
page = this.pages.get('importConfig');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Operation.export: {
|
||||||
|
page = this.pages.get('exportConfig');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (idx === 2) {
|
||||||
|
page = this.pages.get('summary');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page !== undefined) {
|
||||||
|
page.dacFxPage.setupNavigationValidator();
|
||||||
|
page.dacFxPage.onPageEnter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.wizard.pages = [selectOperationWizardPage, deployConfigWizardPage, summaryWizardPage];
|
||||||
|
this.wizard.generateScriptButton.hidden = true;
|
||||||
|
this.wizard.doneButton.onClick(async () => await this.executeOperation());
|
||||||
|
|
||||||
|
this.wizard.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerNavigationValidator(validator: (pageChangeInfo: sqlops.window.modelviewdialog.WizardPageChangeInfo) => boolean) {
|
||||||
|
this.wizard.registerNavigationValidator(validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setDoneButton(operation: Operation): void {
|
||||||
|
switch (operation) {
|
||||||
|
case Operation.deploy: {
|
||||||
|
this.wizard.doneButton.label = localize('dacFx.deployButton', 'Deploy');
|
||||||
|
this.selectedOperation = Operation.deploy;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Operation.extract: {
|
||||||
|
this.wizard.doneButton.label = localize('dacFx.extractButton', 'Extract');
|
||||||
|
this.selectedOperation = Operation.extract;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Operation.import: {
|
||||||
|
this.wizard.doneButton.label = localize('dacFx.importButton', 'Import');
|
||||||
|
this.selectedOperation = Operation.import;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Operation.export: {
|
||||||
|
this.wizard.doneButton.label = localize('dacFx.exportButton', 'Export');
|
||||||
|
this.selectedOperation = Operation.export;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeOperation() {
|
||||||
|
switch (this.selectedOperation) {
|
||||||
|
case Operation.deploy: {
|
||||||
|
await this.deploy();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Operation.extract: {
|
||||||
|
await this.extract();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Operation.import: {
|
||||||
|
await this.import();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Operation.export: {
|
||||||
|
await this.export();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deploy() {
|
||||||
|
let service = await DataTierApplicationWizard.getService();
|
||||||
|
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);
|
||||||
|
if (!result || !result.success) {
|
||||||
|
vscode.window.showErrorMessage(
|
||||||
|
localize('alertData.deployErrorMessage', "Deploy failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async extract() {
|
||||||
|
let service = await DataTierApplicationWizard.getService();
|
||||||
|
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);
|
||||||
|
if (!result || !result.success) {
|
||||||
|
vscode.window.showErrorMessage(
|
||||||
|
localize('alertData.extractErrorMessage', "Extract failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async export() {
|
||||||
|
let service = await DataTierApplicationWizard.getService();
|
||||||
|
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);
|
||||||
|
if (!result || !result.success) {
|
||||||
|
vscode.window.showErrorMessage(
|
||||||
|
localize('alertData.exportErrorMessage', "Export failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async import() {
|
||||||
|
let service = await DataTierApplicationWizard.getService();
|
||||||
|
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);
|
||||||
|
if (!result || !result.success) {
|
||||||
|
vscode.window.showErrorMessage(
|
||||||
|
localize('alertData.importErrorMessage', "Import failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
}
|
||||||
112
extensions/import/src/wizard/pages/dacFxSummaryPage.ts
Normal file
112
extensions/import/src/wizard/pages/dacFxSummaryPage.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 nls from 'vscode-nls';
|
||||||
|
import { DacFxDataModel } from '../api/models';
|
||||||
|
import { DataTierApplicationWizard, Operation } from '../dataTierApplicationWizard';
|
||||||
|
import { BasePage } from '../api/basePage';
|
||||||
|
|
||||||
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
|
export class DacFxSummaryPage extends BasePage {
|
||||||
|
|
||||||
|
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
|
||||||
|
protected readonly instance: DataTierApplicationWizard;
|
||||||
|
protected readonly model: DacFxDataModel;
|
||||||
|
protected readonly view: sqlops.ModelView;
|
||||||
|
|
||||||
|
private form: sqlops.FormContainer;
|
||||||
|
private table: sqlops.TableComponent;
|
||||||
|
private loader: sqlops.LoadingComponent;
|
||||||
|
|
||||||
|
public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) {
|
||||||
|
super();
|
||||||
|
this.instance = instance;
|
||||||
|
this.wizardPage = wizardPage;
|
||||||
|
this.model = model;
|
||||||
|
this.view = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<boolean> {
|
||||||
|
this.table = this.view.modelBuilder.table().component();
|
||||||
|
this.loader = this.view.modelBuilder.loadingComponent().withItem(this.table).component();
|
||||||
|
this.form = this.view.modelBuilder.formContainer().withFormItems(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
component: this.table,
|
||||||
|
title: ''
|
||||||
|
}
|
||||||
|
]
|
||||||
|
).component();
|
||||||
|
await this.view.initializeModel(this.form);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onPageEnter(): Promise<boolean> {
|
||||||
|
this.populateTable();
|
||||||
|
this.loader.loading = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setupNavigationValidator() {
|
||||||
|
this.instance.registerNavigationValidator(() => {
|
||||||
|
if (this.loader.loading) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private populateTable() {
|
||||||
|
let data = [];
|
||||||
|
let targetServer = localize('dacfx.targetServerName', 'Target Server');
|
||||||
|
let targetDatabase = localize('dacfx.targetDatabaseName', 'Target Database');
|
||||||
|
let sourceServer = localize('dacfx.sourceServerName', 'Source Server');
|
||||||
|
let sourceDatabase = localize('dacfx.sourceDatabaseName', 'Source Database');
|
||||||
|
let fileLocation = localize('dacfx.fileLocation', 'File Location');
|
||||||
|
|
||||||
|
switch (this.instance.selectedOperation) {
|
||||||
|
case Operation.deploy: {
|
||||||
|
data = [
|
||||||
|
[targetServer, this.model.serverName],
|
||||||
|
[fileLocation, this.model.filePath],
|
||||||
|
[targetDatabase, this.model.database]];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Operation.extract: {
|
||||||
|
data = [
|
||||||
|
[sourceServer, this.model.serverName],
|
||||||
|
[sourceDatabase, this.model.database],
|
||||||
|
[localize('dacfxExtract.version', 'Version'), this.model.version],
|
||||||
|
[fileLocation, this.model.filePath]];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Operation.import: {
|
||||||
|
data = [
|
||||||
|
[targetServer, this.model.serverName],
|
||||||
|
[fileLocation, this.model.filePath],
|
||||||
|
[targetDatabase, this.model.database]];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Operation.export: {
|
||||||
|
data = [
|
||||||
|
[sourceServer, this.model.serverName],
|
||||||
|
[sourceDatabase, this.model.database],
|
||||||
|
[fileLocation, this.model.filePath]];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.table.updateProperties({
|
||||||
|
data: data,
|
||||||
|
columns: ['Setting', 'Value'],
|
||||||
|
width: 600,
|
||||||
|
height: 200
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
190
extensions/import/src/wizard/pages/deployConfigPage.ts
Normal file
190
extensions/import/src/wizard/pages/deployConfigPage.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 nls from 'vscode-nls';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import { DacFxDataModel } from '../api/models';
|
||||||
|
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
|
||||||
|
import { DacFxConfigPage } from '../api/dacFxConfigPage';
|
||||||
|
|
||||||
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
|
export class DeployConfigPage extends DacFxConfigPage {
|
||||||
|
|
||||||
|
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
|
||||||
|
protected readonly instance: DataTierApplicationWizard;
|
||||||
|
protected readonly model: DacFxDataModel;
|
||||||
|
protected readonly view: sqlops.ModelView;
|
||||||
|
private databaseDropdownComponent: sqlops.FormComponent;
|
||||||
|
private databaseComponent: sqlops.FormComponent;
|
||||||
|
private formBuilder: sqlops.FormBuilder;
|
||||||
|
private form: sqlops.FormContainer;
|
||||||
|
|
||||||
|
public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) {
|
||||||
|
super(instance, wizardPage, model, view);
|
||||||
|
this.fileExtension = '.bacpac';
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<boolean> {
|
||||||
|
let serverComponent = await this.createServerDropdown(true);
|
||||||
|
let fileBrowserComponent = await this.createFileBrowser();
|
||||||
|
this.databaseComponent = await this.createDatabaseTextBox();
|
||||||
|
this.databaseComponent.title = localize('dacFx.databaseNameTextBox', 'Database Name');
|
||||||
|
this.databaseDropdownComponent = await this.createDeployDatabaseDropdown();
|
||||||
|
this.databaseDropdownComponent.title = localize('dacFx.databaseNameDropdown', 'Database Name');
|
||||||
|
let radioButtons = await this.createRadiobuttons();
|
||||||
|
|
||||||
|
this.formBuilder = this.view.modelBuilder.formContainer()
|
||||||
|
.withFormItems(
|
||||||
|
[
|
||||||
|
fileBrowserComponent,
|
||||||
|
serverComponent,
|
||||||
|
radioButtons,
|
||||||
|
this.databaseDropdownComponent
|
||||||
|
], {
|
||||||
|
horizontal: true,
|
||||||
|
componentWidth: 400
|
||||||
|
});
|
||||||
|
|
||||||
|
this.form = this.formBuilder.component();
|
||||||
|
await this.view.initializeModel(this.form);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onPageEnter(): Promise<boolean> {
|
||||||
|
let r1 = await this.populateServerDropdown();
|
||||||
|
let r2 = await this.populateDeployDatabaseDropdown();
|
||||||
|
return r1 && r2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createFileBrowser(): Promise<sqlops.FormComponent> {
|
||||||
|
this.createFileBrowserParts();
|
||||||
|
|
||||||
|
this.fileButton.onDidClick(async (click) => {
|
||||||
|
let fileUris = await vscode.window.showOpenDialog(
|
||||||
|
{
|
||||||
|
canSelectFiles: true,
|
||||||
|
canSelectFolders: false,
|
||||||
|
canSelectMany: false,
|
||||||
|
defaultUri: vscode.Uri.file(os.homedir()),
|
||||||
|
openLabel: localize('dacFxDeploy.openFile', 'Open'),
|
||||||
|
filters: {
|
||||||
|
'dacpac Files': ['dacpac'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fileUris || fileUris.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileUri = fileUris[0];
|
||||||
|
this.fileTextBox.value = fileUri.fsPath;
|
||||||
|
this.model.filePath = fileUri.fsPath;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fileTextBox.onTextChanged(async () => {
|
||||||
|
this.model.filePath = this.fileTextBox.value;
|
||||||
|
this.databaseTextBox.value = this.generateDatabaseName(this.model.filePath);
|
||||||
|
if (!this.model.upgradeExisting) {
|
||||||
|
this.model.database = this.databaseTextBox.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: this.fileTextBox,
|
||||||
|
title: localize('dacFxDeploy.fileTextboxTitle', 'File Location'),
|
||||||
|
actions: [this.fileButton]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createRadiobuttons(): Promise<sqlops.FormComponent> {
|
||||||
|
let upgradeRadioButton = this.view.modelBuilder.radioButton()
|
||||||
|
.withProperties({
|
||||||
|
name: 'updateExisting',
|
||||||
|
label: localize('dacFx.upgradeRadioButtonLabel', 'Upgrade Existing Database'),
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
let newRadioButton = this.view.modelBuilder.radioButton()
|
||||||
|
.withProperties({
|
||||||
|
name: 'updateExisting',
|
||||||
|
label: localize('dacFx.newRadioButtonLabel', 'New Database'),
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
upgradeRadioButton.onDidClick(() => {
|
||||||
|
this.model.upgradeExisting = true;
|
||||||
|
this.formBuilder.removeFormItem(this.databaseComponent);
|
||||||
|
this.formBuilder.addFormItem(this.databaseDropdownComponent, { horizontal: true, componentWidth: 400 });
|
||||||
|
this.model.database = (<sqlops.CategoryValue>this.databaseDropdown.value).name;
|
||||||
|
});
|
||||||
|
|
||||||
|
newRadioButton.onDidClick(() => {
|
||||||
|
this.model.upgradeExisting = false;
|
||||||
|
this.formBuilder.removeFormItem(this.databaseDropdownComponent);
|
||||||
|
this.formBuilder.addFormItem(this.databaseComponent, { horizontal: true, componentWidth: 400 });
|
||||||
|
this.model.database = this.databaseTextBox.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize with upgrade existing true
|
||||||
|
upgradeRadioButton.checked = true;
|
||||||
|
this.model.upgradeExisting = true;
|
||||||
|
|
||||||
|
let flexRadioButtonsModel = this.view.modelBuilder.flexContainer()
|
||||||
|
.withLayout({
|
||||||
|
flexFlow: 'row',
|
||||||
|
}).withItems([
|
||||||
|
upgradeRadioButton, newRadioButton]
|
||||||
|
).component();
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: flexRadioButtonsModel,
|
||||||
|
title: localize('dacFx.targetDatabaseRadioButtonsTitle', 'Target Database')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async createDeployDatabaseDropdown(): Promise<sqlops.FormComponent> {
|
||||||
|
this.databaseDropdown = this.view.modelBuilder.dropDown().withProperties({
|
||||||
|
required: true
|
||||||
|
}).component();
|
||||||
|
// Handle database changes
|
||||||
|
this.databaseDropdown.onValueChanged(async () => {
|
||||||
|
this.model.database = (<sqlops.CategoryValue>this.databaseDropdown.value).name;
|
||||||
|
});
|
||||||
|
this.databaseLoader = this.view.modelBuilder.loadingComponent().withItem(this.databaseDropdown).component();
|
||||||
|
return {
|
||||||
|
component: this.databaseLoader,
|
||||||
|
title: localize('dacFx.targetDatabaseDropdownTitle', 'Database Name')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async populateDeployDatabaseDropdown(): Promise<boolean> {
|
||||||
|
this.databaseLoader.loading = true;
|
||||||
|
this.databaseDropdown.updateProperties({ values: [] });
|
||||||
|
if (!this.model.server) {
|
||||||
|
this.databaseLoader.loading = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let values = await this.getDatabaseValues();
|
||||||
|
|
||||||
|
if (this.model.database === undefined) {
|
||||||
|
this.model.database = values[0].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.databaseDropdown.updateProperties({
|
||||||
|
values: values
|
||||||
|
});
|
||||||
|
this.databaseLoader.loading = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateDatabaseName(filePath: string): string {
|
||||||
|
let result = path.parse(filePath);
|
||||||
|
return result.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
100
extensions/import/src/wizard/pages/exportConfigPage.ts
Normal file
100
extensions/import/src/wizard/pages/exportConfigPage.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 nls from 'vscode-nls';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { DacFxDataModel } from '../api/models';
|
||||||
|
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
|
||||||
|
import { DacFxConfigPage } from '../api/dacFxConfigPage';
|
||||||
|
|
||||||
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
|
export class ExportConfigPage extends DacFxConfigPage {
|
||||||
|
|
||||||
|
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
|
||||||
|
protected readonly instance: DataTierApplicationWizard;
|
||||||
|
protected readonly model: DacFxDataModel;
|
||||||
|
protected readonly view: sqlops.ModelView;
|
||||||
|
|
||||||
|
private form: sqlops.FormContainer;
|
||||||
|
|
||||||
|
public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) {
|
||||||
|
super(instance, wizardPage, model, view);
|
||||||
|
this.fileExtension = '.bacpac';
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<boolean> {
|
||||||
|
let databaseComponent = await this.createDatabaseDropdown();
|
||||||
|
let serverComponent = await this.createServerDropdown(false);
|
||||||
|
let fileBrowserComponent = await this.createFileBrowser();
|
||||||
|
|
||||||
|
this.form = this.view.modelBuilder.formContainer()
|
||||||
|
.withFormItems(
|
||||||
|
[
|
||||||
|
serverComponent,
|
||||||
|
databaseComponent,
|
||||||
|
fileBrowserComponent,
|
||||||
|
], {
|
||||||
|
horizontal: true,
|
||||||
|
componentWidth: 400
|
||||||
|
}).component();
|
||||||
|
await this.view.initializeModel(this.form);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onPageEnter(): Promise<boolean> {
|
||||||
|
let r1 = await this.populateServerDropdown();
|
||||||
|
let r2 = await this.populateDatabaseDropdown();
|
||||||
|
return r1 && r2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setupNavigationValidator() {
|
||||||
|
this.instance.registerNavigationValidator(() => {
|
||||||
|
if (this.databaseLoader.loading) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createFileBrowser(): Promise<sqlops.FormComponent> {
|
||||||
|
this.createFileBrowserParts();
|
||||||
|
|
||||||
|
// default filepath
|
||||||
|
this.fileTextBox.value = this.generateFilePath();
|
||||||
|
this.model.filePath = this.fileTextBox.value;
|
||||||
|
|
||||||
|
this.fileButton.onDidClick(async (click) => {
|
||||||
|
let fileUri = await vscode.window.showSaveDialog(
|
||||||
|
{
|
||||||
|
defaultUri: vscode.Uri.file(this.fileTextBox.value),
|
||||||
|
saveLabel: localize('dacfxExport.saveFile', 'Save'),
|
||||||
|
filters: {
|
||||||
|
'bacpac Files': ['bacpac'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fileUri) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fileTextBox.value = fileUri.fsPath;
|
||||||
|
this.model.filePath = fileUri.fsPath;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fileTextBox.onTextChanged(async () => {
|
||||||
|
this.model.filePath = this.fileTextBox.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: this.fileTextBox,
|
||||||
|
title: localize('dacFxExport.fileTextboxTitle', 'File Location'),
|
||||||
|
actions: [this.fileButton]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
122
extensions/import/src/wizard/pages/extractConfigPage.ts
Normal file
122
extensions/import/src/wizard/pages/extractConfigPage.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 nls from 'vscode-nls';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { DacFxDataModel } from '../api/models';
|
||||||
|
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
|
||||||
|
import { DacFxConfigPage } from '../api/dacFxConfigPage';
|
||||||
|
|
||||||
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
|
export class ExtractConfigPage extends DacFxConfigPage {
|
||||||
|
|
||||||
|
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
|
||||||
|
protected readonly instance: DataTierApplicationWizard;
|
||||||
|
protected readonly model: DacFxDataModel;
|
||||||
|
protected readonly view: sqlops.ModelView;
|
||||||
|
|
||||||
|
private form: sqlops.FormContainer;
|
||||||
|
private versionTextBox: sqlops.InputBoxComponent;
|
||||||
|
|
||||||
|
public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) {
|
||||||
|
super(instance, wizardPage, model, view);
|
||||||
|
this.fileExtension = '.dacpac';
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<boolean> {
|
||||||
|
let databaseComponent = await this.createDatabaseDropdown();
|
||||||
|
let serverComponent = await this.createServerDropdown(false);
|
||||||
|
let fileBrowserComponent = await this.createFileBrowser();
|
||||||
|
let versionComponent = await this.createVersionTextBox();
|
||||||
|
|
||||||
|
this.form = this.view.modelBuilder.formContainer()
|
||||||
|
.withFormItems(
|
||||||
|
[
|
||||||
|
serverComponent,
|
||||||
|
databaseComponent,
|
||||||
|
versionComponent,
|
||||||
|
fileBrowserComponent,
|
||||||
|
], {
|
||||||
|
horizontal: true,
|
||||||
|
componentWidth: 400
|
||||||
|
}).component();
|
||||||
|
await this.view.initializeModel(this.form);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onPageEnter(): Promise<boolean> {
|
||||||
|
let r1 = await this.populateServerDropdown();
|
||||||
|
let r2 = await this.populateDatabaseDropdown();
|
||||||
|
return r1 && r2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setupNavigationValidator() {
|
||||||
|
this.instance.registerNavigationValidator(() => {
|
||||||
|
if (this.databaseLoader.loading) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createFileBrowser(): Promise<sqlops.FormComponent> {
|
||||||
|
this.createFileBrowserParts();
|
||||||
|
|
||||||
|
// default filepath
|
||||||
|
this.fileTextBox.value = this.generateFilePath();
|
||||||
|
this.model.filePath = this.fileTextBox.value;
|
||||||
|
|
||||||
|
this.fileButton.onDidClick(async (click) => {
|
||||||
|
let fileUri = await vscode.window.showSaveDialog(
|
||||||
|
{
|
||||||
|
defaultUri: vscode.Uri.file(this.fileTextBox.value),
|
||||||
|
saveLabel: localize('dacfxExtract.saveFile', 'Save'),
|
||||||
|
filters: {
|
||||||
|
'dacpac Files': ['dacpac'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fileUri) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fileTextBox.value = fileUri.fsPath;
|
||||||
|
this.model.filePath = fileUri.fsPath;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fileTextBox.onTextChanged(async () => {
|
||||||
|
this.model.filePath = this.fileTextBox.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: this.fileTextBox,
|
||||||
|
title: localize('dacFxExtract.fileTextboxTitle', 'File Location'),
|
||||||
|
actions: [this.fileButton]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createVersionTextBox(): Promise<sqlops.FormComponent> {
|
||||||
|
this.versionTextBox = this.view.modelBuilder.inputBox().withProperties({
|
||||||
|
required: true
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
// default filepath
|
||||||
|
this.versionTextBox.value = '1.0.0.0';
|
||||||
|
this.model.version = this.versionTextBox.value;
|
||||||
|
|
||||||
|
this.versionTextBox.onTextChanged(async () => {
|
||||||
|
this.model.version = this.versionTextBox.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: this.versionTextBox,
|
||||||
|
title: localize('dacFxExtract.versionTextboxTitle', 'Version (use x.x.x.x where x is a number)'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,57 +102,9 @@ export class FileConfigPage extends ImportPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async populateServerDropdown(): Promise<boolean> {
|
private async populateServerDropdown(): Promise<boolean> {
|
||||||
let cons = await sqlops.connection.getActiveConnections();
|
let values = await this.getServerValues();
|
||||||
// This user has no active connections ABORT MISSION
|
if (values === undefined) {
|
||||||
if (!cons || cons.length === 0) {
|
return false;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let count = -1;
|
|
||||||
let idx = -1;
|
|
||||||
|
|
||||||
|
|
||||||
let values = cons.map(c => {
|
|
||||||
// Handle the code to remember what the user's choice was from before
|
|
||||||
count++;
|
|
||||||
if (idx === -1) {
|
|
||||||
if (this.model.server && c.connectionId === this.model.server.connectionId) {
|
|
||||||
idx = count;
|
|
||||||
} else if (this.model.serverId && c.connectionId === this.model.serverId) {
|
|
||||||
idx = count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let db = c.options.databaseDisplayName;
|
|
||||||
let usr = c.options.user;
|
|
||||||
let srv = c.options.server;
|
|
||||||
|
|
||||||
if (!db) {
|
|
||||||
db = '<default>';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!usr) {
|
|
||||||
usr = 'default';
|
|
||||||
}
|
|
||||||
|
|
||||||
let finalName = `${srv}, ${db} (${usr})`;
|
|
||||||
return {
|
|
||||||
connection: c,
|
|
||||||
displayName: finalName,
|
|
||||||
name: c.connectionId
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (idx >= 0) {
|
|
||||||
let tmp = values[0];
|
|
||||||
values[0] = values[idx];
|
|
||||||
values[idx] = tmp;
|
|
||||||
} else {
|
|
||||||
delete this.model.server;
|
|
||||||
delete this.model.serverId;
|
|
||||||
delete this.model.database;
|
|
||||||
delete this.model.schema;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.model.server = values[0].connection;
|
this.model.server = values[0].connection;
|
||||||
@@ -195,29 +147,7 @@ export class FileConfigPage extends ImportPage {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let values = await this.getDatabaseValues();
|
||||||
let idx = -1;
|
|
||||||
let count = -1;
|
|
||||||
let values = (await sqlops.connection.listDatabases(this.model.server.connectionId)).map(db => {
|
|
||||||
count++;
|
|
||||||
if (this.model.database && db === this.model.database) {
|
|
||||||
idx = count;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
displayName: db,
|
|
||||||
name: db
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (idx >= 0) {
|
|
||||||
let tmp = values[0];
|
|
||||||
values[0] = values[idx];
|
|
||||||
values[idx] = tmp;
|
|
||||||
} else {
|
|
||||||
delete this.model.database;
|
|
||||||
delete this.model.schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.model.database = values[0].name;
|
this.model.database = values[0].name;
|
||||||
|
|
||||||
@@ -377,6 +307,18 @@ export class FileConfigPage extends ImportPage {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected deleteServerValues() {
|
||||||
|
delete this.model.server;
|
||||||
|
delete this.model.serverId;
|
||||||
|
delete this.model.database;
|
||||||
|
delete this.model.schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected deleteDatabaseValues() {
|
||||||
|
delete this.model.database;
|
||||||
|
delete this.model.schema;
|
||||||
|
}
|
||||||
|
|
||||||
// private async populateTableNames(): Promise<boolean> {
|
// private async populateTableNames(): Promise<boolean> {
|
||||||
// this.tableNames = [];
|
// this.tableNames = [];
|
||||||
// let databaseName = (<sqlops.CategoryValue>this.databaseDropdown.value).name;
|
// let databaseName = (<sqlops.CategoryValue>this.databaseDropdown.value).name;
|
||||||
|
|||||||
101
extensions/import/src/wizard/pages/importConfigPage.ts
Normal file
101
extensions/import/src/wizard/pages/importConfigPage.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
import * as sqlops from 'sqlops';
|
||||||
|
import * as nls from 'vscode-nls';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import { DacFxDataModel } from '../api/models';
|
||||||
|
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
|
||||||
|
import { DacFxConfigPage } from '../api/dacFxConfigPage';
|
||||||
|
|
||||||
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
|
export class ImportConfigPage extends DacFxConfigPage {
|
||||||
|
|
||||||
|
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
|
||||||
|
protected readonly instance: DataTierApplicationWizard;
|
||||||
|
protected readonly model: DacFxDataModel;
|
||||||
|
protected readonly view: sqlops.ModelView;
|
||||||
|
|
||||||
|
private form: sqlops.FormContainer;
|
||||||
|
|
||||||
|
public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) {
|
||||||
|
super(instance, wizardPage, model, view);
|
||||||
|
this.fileExtension = '.bacpac';
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<boolean> {
|
||||||
|
let databaseComponent = await this.createDatabaseTextBox();
|
||||||
|
let serverComponent = await this.createServerDropdown(true);
|
||||||
|
let fileBrowserComponent = await this.createFileBrowser();
|
||||||
|
|
||||||
|
this.form = this.view.modelBuilder.formContainer()
|
||||||
|
.withFormItems(
|
||||||
|
[
|
||||||
|
fileBrowserComponent,
|
||||||
|
serverComponent,
|
||||||
|
databaseComponent,
|
||||||
|
], {
|
||||||
|
horizontal: true,
|
||||||
|
componentWidth: 400
|
||||||
|
}).component();
|
||||||
|
await this.view.initializeModel(this.form);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onPageEnter(): Promise<boolean> {
|
||||||
|
let r1 = await this.populateServerDropdown();
|
||||||
|
return r1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createFileBrowser(): Promise<sqlops.FormComponent> {
|
||||||
|
this.createFileBrowserParts();
|
||||||
|
|
||||||
|
this.fileButton.onDidClick(async (click) => {
|
||||||
|
let fileUris = await vscode.window.showOpenDialog(
|
||||||
|
{
|
||||||
|
canSelectFiles: true,
|
||||||
|
canSelectFolders: false,
|
||||||
|
canSelectMany: false,
|
||||||
|
defaultUri: vscode.Uri.file(os.homedir()),
|
||||||
|
openLabel: localize('dacFxImport.openFile', 'Open'),
|
||||||
|
filters: {
|
||||||
|
'bacpac Files': ['bacpac'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fileUris || fileUris.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileUri = fileUris[0];
|
||||||
|
this.fileTextBox.value = fileUri.fsPath;
|
||||||
|
this.model.filePath = fileUri.fsPath;
|
||||||
|
this.model.database = this.generateDatabaseName(this.model.filePath);
|
||||||
|
this.databaseTextBox.value = this.model.database;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fileTextBox.onTextChanged(async () => {
|
||||||
|
this.model.filePath = this.fileTextBox.value;
|
||||||
|
this.model.database = this.generateDatabaseName(this.model.filePath);
|
||||||
|
this.databaseTextBox.value = this.model.database;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: this.fileTextBox,
|
||||||
|
title: localize('dacFxImport.fileTextboxTitle', 'File Location'),
|
||||||
|
actions: [this.fileButton]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateDatabaseName(filePath: string): string {
|
||||||
|
let result = path.parse(filePath);
|
||||||
|
return result.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
174
extensions/import/src/wizard/pages/selectOperationpage.ts
Normal file
174
extensions/import/src/wizard/pages/selectOperationpage.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 nls from 'vscode-nls';
|
||||||
|
import { DacFxDataModel } from '../api/models';
|
||||||
|
import { DataTierApplicationWizard, Operation } from '../dataTierApplicationWizard';
|
||||||
|
import { BasePage } from '../api/basePage';
|
||||||
|
|
||||||
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
|
export class SelectOperationPage extends BasePage {
|
||||||
|
|
||||||
|
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
|
||||||
|
protected readonly instance: DataTierApplicationWizard;
|
||||||
|
protected readonly model: DacFxDataModel;
|
||||||
|
protected readonly view: sqlops.ModelView;
|
||||||
|
|
||||||
|
private deployRadioButton: sqlops.RadioButtonComponent;
|
||||||
|
private extractRadioButton: sqlops.RadioButtonComponent;
|
||||||
|
private importRadioButton: sqlops.RadioButtonComponent;
|
||||||
|
private exportRadioButton: sqlops.RadioButtonComponent;
|
||||||
|
private form: sqlops.FormContainer;
|
||||||
|
|
||||||
|
public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) {
|
||||||
|
super();
|
||||||
|
this.instance = instance;
|
||||||
|
this.wizardPage = wizardPage;
|
||||||
|
this.model = model;
|
||||||
|
this.view = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<boolean> {
|
||||||
|
let deployComponent = await this.createDeployRadioButton();
|
||||||
|
let extractComponent = await this.createExtractRadioButton();
|
||||||
|
let importComponent = await this.createImportRadioButton();
|
||||||
|
let exportComponent = await this.createExportRadioButton();
|
||||||
|
|
||||||
|
this.form = this.view.modelBuilder.formContainer()
|
||||||
|
.withFormItems(
|
||||||
|
[
|
||||||
|
deployComponent,
|
||||||
|
extractComponent,
|
||||||
|
importComponent,
|
||||||
|
exportComponent
|
||||||
|
], {
|
||||||
|
horizontal: true
|
||||||
|
}).component();
|
||||||
|
await this.view.initializeModel(this.form);
|
||||||
|
|
||||||
|
// default have the first radio button checked
|
||||||
|
this.deployRadioButton.checked = true;
|
||||||
|
this.instance.setDoneButton(Operation.deploy);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onPageEnter(): Promise<boolean> {
|
||||||
|
let numPages = this.instance.wizard.pages.length;
|
||||||
|
for (let i = numPages - 1; i > 2; --i) {
|
||||||
|
await this.instance.wizard.removePage(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createDeployRadioButton(): Promise<sqlops.FormComponent> {
|
||||||
|
this.deployRadioButton = this.view.modelBuilder.radioButton()
|
||||||
|
.withProperties({
|
||||||
|
name: 'selectedOperation',
|
||||||
|
label: localize('dacFx.deployRadioButtonLabel', 'Deploy a data-tier application .dacpac file to an instance of SQL Server [Deploy Dacpac]'),
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.deployRadioButton.onDidClick(() => {
|
||||||
|
// remove the previous page
|
||||||
|
this.instance.wizard.removePage(1);
|
||||||
|
|
||||||
|
// add deploy page
|
||||||
|
let page = this.instance.pages.get('deployConfig');
|
||||||
|
this.instance.wizard.addPage(page.wizardPage, 1);
|
||||||
|
|
||||||
|
// change button text and operation
|
||||||
|
this.instance.setDoneButton(Operation.deploy);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: this.deployRadioButton,
|
||||||
|
title: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createExtractRadioButton(): Promise<sqlops.FormComponent> {
|
||||||
|
this.extractRadioButton = this.view.modelBuilder.radioButton()
|
||||||
|
.withProperties({
|
||||||
|
name: 'selectedOperation',
|
||||||
|
label: localize('dacFx.extractRadioButtonLabel', 'Extract a data-tier application from an instance of SQL Server to a .dacpac file [Extract Dacpac]'),
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.extractRadioButton.onDidClick(() => {
|
||||||
|
// remove the previous pages
|
||||||
|
this.instance.wizard.removePage(1);
|
||||||
|
|
||||||
|
// add the extract page
|
||||||
|
let page = this.instance.pages.get('extractConfig');
|
||||||
|
this.instance.wizard.addPage(page.wizardPage, 1);
|
||||||
|
|
||||||
|
// change button text and operation
|
||||||
|
this.instance.setDoneButton(Operation.extract);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: this.extractRadioButton,
|
||||||
|
title: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createImportRadioButton(): Promise<sqlops.FormComponent> {
|
||||||
|
this.importRadioButton = this.view.modelBuilder.radioButton()
|
||||||
|
.withProperties({
|
||||||
|
name: 'selectedOperation',
|
||||||
|
label: localize('dacFx.importRadioButtonLabel', 'Create a database from a .bacpac file [Import Bacpac]'),
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.importRadioButton.onDidClick(() => {
|
||||||
|
// remove the previous page
|
||||||
|
this.instance.wizard.removePage(1);
|
||||||
|
|
||||||
|
// add the import page
|
||||||
|
let page = this.instance.pages.get('importConfig');
|
||||||
|
this.instance.wizard.addPage(page.wizardPage, 1);
|
||||||
|
|
||||||
|
// change button text and operation
|
||||||
|
this.instance.setDoneButton(Operation.import);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: this.importRadioButton,
|
||||||
|
title: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createExportRadioButton(): Promise<sqlops.FormComponent> {
|
||||||
|
this.exportRadioButton = this.view.modelBuilder.radioButton()
|
||||||
|
.withProperties({
|
||||||
|
name: 'selectedOperation',
|
||||||
|
label: localize('dacFx.exportRadioButtonLabel', 'Export the schema and data from a database to the logical .bacpac file format [Export Bacpac]'),
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.exportRadioButton.onDidClick(() => {
|
||||||
|
// remove the 2 previous pages
|
||||||
|
this.instance.wizard.removePage(1);
|
||||||
|
|
||||||
|
// add the export pages
|
||||||
|
let page = this.instance.pages.get('exportConfig');
|
||||||
|
this.instance.wizard.addPage(page.wizardPage, 1);
|
||||||
|
|
||||||
|
// change button text and operation
|
||||||
|
this.instance.setDoneButton(Operation.export);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: this.exportRadioButton,
|
||||||
|
title: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public setupNavigationValidator() {
|
||||||
|
this.instance.registerNavigationValidator(() => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -285,6 +285,10 @@
|
|||||||
{
|
{
|
||||||
"displayName": "Windows Authentication",
|
"displayName": "Windows Authentication",
|
||||||
"name": "Integrated"
|
"name": "Integrated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"displayName": "Azure Active Directory - Universal with MFA support",
|
||||||
|
"name": "AzureMFA"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"isRequired": true,
|
"isRequired": true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
|
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
|
||||||
"version": "1.5.0-alpha.58",
|
"version": "1.5.0-alpha.63",
|
||||||
"downloadFileNames": {
|
"downloadFileNames": {
|
||||||
"Windows_86": "win-x86-netcoreapp2.2.zip",
|
"Windows_86": "win-x86-netcoreapp2.2.zip",
|
||||||
"Windows_64": "win-x64-netcoreapp2.2.zip",
|
"Windows_64": "win-x64-netcoreapp2.2.zip",
|
||||||
|
|||||||
@@ -291,3 +291,60 @@ export namespace DeleteAgentJobScheduleRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------- < Agent Management > ------------------------------------
|
// ------------------------------- < Agent Management > ------------------------------------
|
||||||
|
|
||||||
|
// ------------------------------- < DacFx > ------------------------------------
|
||||||
|
|
||||||
|
export enum TaskExecutionMode {
|
||||||
|
execute = 0,
|
||||||
|
script = 1,
|
||||||
|
executeAndScript = 2,
|
||||||
|
}
|
||||||
|
export interface ExportParams {
|
||||||
|
databaseName: string;
|
||||||
|
packageFilePath: string;
|
||||||
|
ownerUri: string;
|
||||||
|
taskExecutionMode: TaskExecutionMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImportParams {
|
||||||
|
packageFilePath: string;
|
||||||
|
databaseName: string;
|
||||||
|
ownerUri: string;
|
||||||
|
taskExecutionMode: TaskExecutionMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface ExtractParams {
|
||||||
|
databaseName: string;
|
||||||
|
packageFilePath: string;
|
||||||
|
applicationName: string;
|
||||||
|
applicationVersion: string;
|
||||||
|
ownerUri: string;
|
||||||
|
taskExecutionMode: TaskExecutionMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeployParams {
|
||||||
|
packageFilePath: string;
|
||||||
|
databaseName: string;
|
||||||
|
upgradeExisting: boolean;
|
||||||
|
ownerUri: string;
|
||||||
|
taskExecutionMode: TaskExecutionMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace ExportRequest {
|
||||||
|
export const type = new RequestType<ExportParams, sqlops.DacFxResult, void, void>('dacfx/export');
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace ImportRequest {
|
||||||
|
export const type = new RequestType<ImportParams, sqlops.DacFxResult, void, void>('dacfx/import');
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace ExtractRequest {
|
||||||
|
export const type = new RequestType<ExtractParams, sqlops.DacFxResult, void, void>('dacfx/extract');
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace DeployRequest {
|
||||||
|
export const type = new RequestType<DeployParams, sqlops.DacFxResult, void, void>('dacfx/deploy');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------- < DacFx > ------------------------------------
|
||||||
@@ -8,7 +8,7 @@ import { SqlOpsDataClient, SqlOpsFeature } from 'dataprotocol-client';
|
|||||||
import { ClientCapabilities, StaticFeature, RPCMessageType, ServerCapabilities } from 'vscode-languageclient';
|
import { ClientCapabilities, StaticFeature, RPCMessageType, ServerCapabilities } from 'vscode-languageclient';
|
||||||
import { Disposable } from 'vscode';
|
import { Disposable } from 'vscode';
|
||||||
import { Telemetry } from './telemetry';
|
import { Telemetry } from './telemetry';
|
||||||
import * as contracts from './contracts';
|
import * as contracts from './contracts';
|
||||||
import * as sqlops from 'sqlops';
|
import * as sqlops from 'sqlops';
|
||||||
import * as Utils from './utils';
|
import * as Utils from './utils';
|
||||||
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
|
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
|
||||||
@@ -28,6 +28,94 @@ export class TelemetryFeature implements StaticFeature {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class DacFxServicesFeature extends SqlOpsFeature<undefined> {
|
||||||
|
private static readonly messageTypes: RPCMessageType[] = [
|
||||||
|
contracts.ExportRequest.type,
|
||||||
|
contracts.ImportRequest.type,
|
||||||
|
contracts.ExtractRequest.type,
|
||||||
|
contracts.DeployRequest.type
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(client: SqlOpsDataClient) {
|
||||||
|
super(client, DacFxServicesFeature.messageTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public fillClientCapabilities(capabilities: ClientCapabilities): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
public initialize(capabilities: ServerCapabilities): void {
|
||||||
|
this.register(this.messages, {
|
||||||
|
id: UUID.generateUuid(),
|
||||||
|
registerOptions: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected registerProvider(options: undefined): Disposable {
|
||||||
|
const client = this._client;
|
||||||
|
let self = this;
|
||||||
|
|
||||||
|
let exportBacpac = (databaseName: string, packageFilePath: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> => {
|
||||||
|
let params: contracts.ExportParams = { databaseName: databaseName, packageFilePath: packageFilePath, ownerUri: ownerUri, taskExecutionMode: taskExecutionMode };
|
||||||
|
return client.sendRequest(contracts.ExportRequest.type, params).then(
|
||||||
|
r => {
|
||||||
|
return r;
|
||||||
|
},
|
||||||
|
e => {
|
||||||
|
client.logFailedRequest(contracts.ExportRequest.type, e);
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let importBacpac = (packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> => {
|
||||||
|
let params: contracts.ImportParams = { packageFilePath: packageFilePath, databaseName: databaseName, ownerUri: ownerUri, taskExecutionMode: taskExecutionMode };
|
||||||
|
return client.sendRequest(contracts.ImportRequest.type, params).then(
|
||||||
|
r => {
|
||||||
|
return r;
|
||||||
|
},
|
||||||
|
e => {
|
||||||
|
client.logFailedRequest(contracts.ImportRequest.type, e);
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let extractDacpac = (databaseName: string, packageFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> => {
|
||||||
|
let params: contracts.ExtractParams = { databaseName: databaseName, packageFilePath: packageFilePath, applicationName: applicationName, applicationVersion: applicationVersion, ownerUri: ownerUri, taskExecutionMode: taskExecutionMode };
|
||||||
|
return client.sendRequest(contracts.ExtractRequest.type, params).then(
|
||||||
|
r => {
|
||||||
|
return r;
|
||||||
|
},
|
||||||
|
e => {
|
||||||
|
client.logFailedRequest(contracts.ExtractRequest.type, e);
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let deployDacpac = (packageFilePath: string, targetDatabaseName: string, upgradeExisting: boolean, ownerUri: string, taskExecutionMode: sqlops.TaskExecutionMode): Thenable<sqlops.DacFxResult> => {
|
||||||
|
let params: contracts.DeployParams = { packageFilePath: packageFilePath, databaseName: targetDatabaseName, upgradeExisting: upgradeExisting, ownerUri: ownerUri, taskExecutionMode: taskExecutionMode };
|
||||||
|
return client.sendRequest(contracts.DeployRequest.type, params).then(
|
||||||
|
r => {
|
||||||
|
return r;
|
||||||
|
},
|
||||||
|
e => {
|
||||||
|
client.logFailedRequest(contracts.DeployRequest.type, e);
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return sqlops.dataprotocol.registerDacFxServicesProvider({
|
||||||
|
providerId: client.providerId,
|
||||||
|
exportBacpac,
|
||||||
|
importBacpac,
|
||||||
|
extractDacpac,
|
||||||
|
deployDacpac
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class AgentServicesFeature extends SqlOpsFeature<undefined> {
|
export class AgentServicesFeature extends SqlOpsFeature<undefined> {
|
||||||
private static readonly messagesTypes: RPCMessageType[] = [
|
private static readonly messagesTypes: RPCMessageType[] = [
|
||||||
contracts.AgentJobsRequest.type,
|
contracts.AgentJobsRequest.type,
|
||||||
@@ -229,7 +317,7 @@ export class AgentServicesFeature extends SqlOpsFeature<undefined> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Alert management methods
|
// Alert management methods
|
||||||
let getAlerts = (ownerUri: string): Thenable<sqlops.AgentAlertsResult> => {
|
let getAlerts = (ownerUri: string): Thenable<sqlops.AgentAlertsResult> => {
|
||||||
let params: contracts.AgentAlertsParams = {
|
let params: contracts.AgentAlertsParams = {
|
||||||
ownerUri: ownerUri
|
ownerUri: ownerUri
|
||||||
};
|
};
|
||||||
@@ -299,7 +387,7 @@ export class AgentServicesFeature extends SqlOpsFeature<undefined> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Operator management methods
|
// Operator management methods
|
||||||
let getOperators = (ownerUri: string): Thenable<sqlops.AgentOperatorsResult> => {
|
let getOperators = (ownerUri: string): Thenable<sqlops.AgentOperatorsResult> => {
|
||||||
let params: contracts.AgentOperatorsParams = {
|
let params: contracts.AgentOperatorsParams = {
|
||||||
ownerUri: ownerUri
|
ownerUri: ownerUri
|
||||||
};
|
};
|
||||||
@@ -369,7 +457,7 @@ export class AgentServicesFeature extends SqlOpsFeature<undefined> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Proxy management methods
|
// Proxy management methods
|
||||||
let getProxies = (ownerUri: string): Thenable<sqlops.AgentProxiesResult> => {
|
let getProxies = (ownerUri: string): Thenable<sqlops.AgentProxiesResult> => {
|
||||||
let params: contracts.AgentProxiesParams = {
|
let params: contracts.AgentProxiesParams = {
|
||||||
ownerUri: ownerUri
|
ownerUri: ownerUri
|
||||||
};
|
};
|
||||||
@@ -439,7 +527,7 @@ export class AgentServicesFeature extends SqlOpsFeature<undefined> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Agent Credential Method
|
// Agent Credential Method
|
||||||
let getCredentials = (ownerUri: string): Thenable<sqlops.GetCredentialsResult> => {
|
let getCredentials = (ownerUri: string): Thenable<sqlops.GetCredentialsResult> => {
|
||||||
let params: contracts.GetCredentialsParams = {
|
let params: contracts.GetCredentialsParams = {
|
||||||
ownerUri: ownerUri
|
ownerUri: ownerUri
|
||||||
};
|
};
|
||||||
@@ -455,7 +543,7 @@ export class AgentServicesFeature extends SqlOpsFeature<undefined> {
|
|||||||
|
|
||||||
|
|
||||||
// Job Schedule management methods
|
// Job Schedule management methods
|
||||||
let getJobSchedules = (ownerUri: string): Thenable<sqlops.AgentJobSchedulesResult> => {
|
let getJobSchedules = (ownerUri: string): Thenable<sqlops.AgentJobSchedulesResult> => {
|
||||||
let params: contracts.AgentJobScheduleParams = {
|
let params: contracts.AgentJobScheduleParams = {
|
||||||
ownerUri: ownerUri
|
ownerUri: ownerUri
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { CredentialStore } from './credentialstore/credentialstore';
|
|||||||
import { AzureResourceProvider } from './resourceProvider/resourceProvider';
|
import { AzureResourceProvider } from './resourceProvider/resourceProvider';
|
||||||
import * as Utils from './utils';
|
import * as Utils from './utils';
|
||||||
import { Telemetry, LanguageClientErrorHandler } from './telemetry';
|
import { Telemetry, LanguageClientErrorHandler } from './telemetry';
|
||||||
import { TelemetryFeature, AgentServicesFeature } from './features';
|
import { TelemetryFeature, AgentServicesFeature, DacFxServicesFeature } from './features';
|
||||||
|
|
||||||
const baseConfig = require('./config.json');
|
const baseConfig = require('./config.json');
|
||||||
const outputChannel = vscode.window.createOutputChannel(Constants.serviceName);
|
const outputChannel = vscode.window.createOutputChannel(Constants.serviceName);
|
||||||
@@ -55,7 +55,8 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
// we only want to add new features
|
// we only want to add new features
|
||||||
...SqlOpsDataClient.defaultFeatures,
|
...SqlOpsDataClient.defaultFeatures,
|
||||||
TelemetryFeature,
|
TelemetryFeature,
|
||||||
AgentServicesFeature
|
AgentServicesFeature,
|
||||||
|
DacFxServicesFeature,
|
||||||
],
|
],
|
||||||
outputChannel: new CustomOutputChannel()
|
outputChannel: new CustomOutputChannel()
|
||||||
};
|
};
|
||||||
|
|||||||
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",
|
"name": "profiler",
|
||||||
"displayName": "SQL Server Profiler",
|
"displayName": "SQL Server Profiler",
|
||||||
"description": "SQL Server Profiler for Azure Data Studio",
|
"description": "SQL Server Profiler for Azure Data Studio",
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"publisher": "Microsoft",
|
"publisher": "Microsoft",
|
||||||
"preview": true,
|
"preview": true,
|
||||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",
|
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",
|
||||||
@@ -26,8 +26,7 @@
|
|||||||
"Microsoft.mssql"
|
"Microsoft.mssql"
|
||||||
],
|
],
|
||||||
"contributes": {
|
"contributes": {
|
||||||
"commands": [
|
"commands": [{
|
||||||
{
|
|
||||||
"command": "profiler.newProfiler",
|
"command": "profiler.newProfiler",
|
||||||
"title": "Launch Profiler",
|
"title": "Launch Profiler",
|
||||||
"category": "Profiler"
|
"category": "Profiler"
|
||||||
@@ -49,13 +48,23 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"menus": {
|
"menus": {
|
||||||
"objectExplorer/item/context": [
|
"commandPalette": [{
|
||||||
|
"command": "profiler.start",
|
||||||
|
"when": "False"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "profiler.newProfiler",
|
"command": "profiler.stop",
|
||||||
"when": "connectionProvider == MSSQL && nodeType && nodeType == Server",
|
"when": "False"
|
||||||
"group": "profiler"
|
}, {
|
||||||
|
"command": "profiler.openCreateSessionDialog",
|
||||||
|
"when": "False"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"objectExplorer/item/context": [{
|
||||||
|
"command": "profiler.newProfiler",
|
||||||
|
"when": "connectionProvider == MSSQL && nodeType && nodeType == Server",
|
||||||
|
"group": "profiler"
|
||||||
|
}]
|
||||||
},
|
},
|
||||||
"outputChannels": [
|
"outputChannels": [
|
||||||
"sqlprofiler"
|
"sqlprofiler"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
18
package.json
18
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "azuredatastudio",
|
"name": "azuredatastudio",
|
||||||
"version": "1.3.3",
|
"version": "1.3.7",
|
||||||
"distro": "8c3e97e3425cc9814496472ab73e076de2ba99ee",
|
"distro": "8c3e97e3425cc9814496472ab73e076de2ba99ee",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Microsoft Corporation"
|
"name": "Microsoft Corporation"
|
||||||
@@ -90,6 +90,7 @@
|
|||||||
"@types/mocha": "2.2.39",
|
"@types/mocha": "2.2.39",
|
||||||
"@types/sanitize-html": "^1.18.2",
|
"@types/sanitize-html": "^1.18.2",
|
||||||
"@types/semver": "5.3.30",
|
"@types/semver": "5.3.30",
|
||||||
|
"@types/should": "^13.0.0",
|
||||||
"@types/sinon": "1.16.34",
|
"@types/sinon": "1.16.34",
|
||||||
"@types/winreg": "^1.2.30",
|
"@types/winreg": "^1.2.30",
|
||||||
"asar": "^0.14.0",
|
"asar": "^0.14.0",
|
||||||
@@ -102,12 +103,12 @@
|
|||||||
"documentdb": "^1.5.1",
|
"documentdb": "^1.5.1",
|
||||||
"electron-mksnapshot": "~1.7.0",
|
"electron-mksnapshot": "~1.7.0",
|
||||||
"eslint": "^3.4.0",
|
"eslint": "^3.4.0",
|
||||||
"event-stream": "^3.3.4",
|
"event-stream": "3.3.4",
|
||||||
"express": "^4.13.1",
|
"express": "^4.13.1",
|
||||||
"glob": "^5.0.13",
|
"glob": "^5.0.13",
|
||||||
"gulp": "^3.9.1",
|
"gulp": "^3.9.1",
|
||||||
"gulp-atom-electron": "^1.16.1",
|
"gulp-atom-electron": "^1.19.2",
|
||||||
"gulp-azure-storage": "^0.7.0",
|
"gulp-azure-storage": "^0.8.2",
|
||||||
"gulp-bom": "^1.0.0",
|
"gulp-bom": "^1.0.0",
|
||||||
"gulp-buffer": "0.0.2",
|
"gulp-buffer": "0.0.2",
|
||||||
"gulp-cli": "^2.0.1",
|
"gulp-cli": "^2.0.1",
|
||||||
@@ -120,7 +121,7 @@
|
|||||||
"gulp-json-editor": "^2.2.1",
|
"gulp-json-editor": "^2.2.1",
|
||||||
"gulp-mocha": "^2.1.3",
|
"gulp-mocha": "^2.1.3",
|
||||||
"gulp-plumber": "^1.2.0",
|
"gulp-plumber": "^1.2.0",
|
||||||
"gulp-remote-src": "^0.4.0",
|
"gulp-remote-src": "^0.4.4",
|
||||||
"gulp-rename": "^1.2.0",
|
"gulp-rename": "^1.2.0",
|
||||||
"gulp-replace": "^0.5.4",
|
"gulp-replace": "^0.5.4",
|
||||||
"gulp-shell": "^0.5.2",
|
"gulp-shell": "^0.5.2",
|
||||||
@@ -129,7 +130,7 @@
|
|||||||
"gulp-tslint": "^8.1.2",
|
"gulp-tslint": "^8.1.2",
|
||||||
"gulp-uglify": "^3.0.0",
|
"gulp-uglify": "^3.0.0",
|
||||||
"gulp-util": "^3.0.6",
|
"gulp-util": "^3.0.6",
|
||||||
"gulp-vinyl-zip": "^1.2.2",
|
"gulp-vinyl-zip": "^2.1.2",
|
||||||
"husky": "^0.13.1",
|
"husky": "^0.13.1",
|
||||||
"innosetup-compiler": "^5.5.60",
|
"innosetup-compiler": "^5.5.60",
|
||||||
"is": "^3.1.0",
|
"is": "^3.1.0",
|
||||||
@@ -148,8 +149,10 @@
|
|||||||
"queue": "3.0.6",
|
"queue": "3.0.6",
|
||||||
"remap-istanbul": "^0.6.4",
|
"remap-istanbul": "^0.6.4",
|
||||||
"rimraf": "^2.2.8",
|
"rimraf": "^2.2.8",
|
||||||
|
"should": "^13.2.3",
|
||||||
"sinon": "^1.17.2",
|
"sinon": "^1.17.2",
|
||||||
"source-map": "^0.4.4",
|
"source-map": "^0.4.4",
|
||||||
|
"temp-write": "^3.4.0",
|
||||||
"tslint": "^5.9.1",
|
"tslint": "^5.9.1",
|
||||||
"typemoq": "^0.3.2",
|
"typemoq": "^0.3.2",
|
||||||
"typescript": "2.9.2",
|
"typescript": "2.9.2",
|
||||||
@@ -174,6 +177,7 @@
|
|||||||
"windows-process-tree": "0.2.2"
|
"windows-process-tree": "0.2.2"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"rc": "1.2.8"
|
"rc": "1.2.8",
|
||||||
|
"event-stream": "3.3.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import * as types from 'vs/base/common/types';
|
|||||||
|
|
||||||
import * as sqlops from 'sqlops';
|
import * as sqlops from 'sqlops';
|
||||||
|
|
||||||
export function appendRow(container: Builder, label: string, labelClass: string, cellContainerClass: string): Builder {
|
export function appendRow(container: Builder, label: string, labelClass: string, cellContainerClass: string, rowContainerClass?: string): Builder {
|
||||||
let cellContainer: Builder;
|
let cellContainer: Builder;
|
||||||
container.element('tr', {}, (rowContainer) => {
|
let rowAttributes = rowContainerClass ? { class: rowContainerClass } : {};
|
||||||
|
container.element('tr', rowAttributes, (rowContainer) => {
|
||||||
rowContainer.element('td', { class: labelClass }, (labelCellContainer) => {
|
rowContainer.element('td', { class: labelClass }, (labelCellContainer) => {
|
||||||
labelCellContainer.div({}, (labelContainer) => {
|
labelCellContainer.div({}, (labelContainer) => {
|
||||||
labelContainer.text(label);
|
labelContainer.text(label);
|
||||||
|
|||||||
@@ -75,7 +75,12 @@ export class SelectBox extends vsSelectBox {
|
|||||||
|
|
||||||
// explicitly set the accessible role so that the screen readers can read the control type properly
|
// explicitly set the accessible role so that the screen readers can read the control type properly
|
||||||
this.selectElement.setAttribute('role', 'combobox');
|
this.selectElement.setAttribute('role', 'combobox');
|
||||||
|
|
||||||
this._selectBoxOptions = selectBoxOptions;
|
this._selectBoxOptions = selectBoxOptions;
|
||||||
|
var focusTracker = dom.trackFocus(this.selectElement);
|
||||||
|
this._register(focusTracker);
|
||||||
|
this._register(focusTracker.onDidBlur(() => this._hideMessage()));
|
||||||
|
this._register(focusTracker.onDidFocus(() => this._showMessage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public style(styles: ISelectBoxStyles): void {
|
public style(styles: ISelectBoxStyles): void {
|
||||||
@@ -142,6 +147,10 @@ export class SelectBox extends vsSelectBox {
|
|||||||
this.applyStyles();
|
this.applyStyles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public hasFocus(): boolean {
|
||||||
|
return document.activeElement === this.selectElement;
|
||||||
|
}
|
||||||
|
|
||||||
public showMessage(message: IMessage): void {
|
public showMessage(message: IMessage): void {
|
||||||
this.message = message;
|
this.message = message;
|
||||||
|
|
||||||
@@ -163,7 +172,9 @@ export class SelectBox extends vsSelectBox {
|
|||||||
|
|
||||||
aria.alert(alertText);
|
aria.alert(alertText);
|
||||||
|
|
||||||
this._showMessage();
|
if (this.hasFocus()) {
|
||||||
|
this._showMessage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public _showMessage(): void {
|
public _showMessage(): void {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export class RowNumberColumn<T> implements Slick.Plugin<T> {
|
|||||||
width: this.currentColumnWidth,
|
width: this.currentColumnWidth,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
cssClass: this.options.cssClass,
|
cssClass: this.options.cssClass,
|
||||||
focusable: true,
|
focusable: false,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
formatter: (r, c, v, cd, dc) => this.formatter(r, c, v, cd, dc)
|
formatter: (r, c, v, cd, dc) => this.formatter(r, c, v, cd, dc)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export const NewQuery = 'NewQuery';
|
|||||||
export const FirewallRuleRequested = 'FirewallRuleCreated';
|
export const FirewallRuleRequested = 'FirewallRuleCreated';
|
||||||
export const DashboardNavigated = 'DashboardNavigated';
|
export const DashboardNavigated = 'DashboardNavigated';
|
||||||
|
|
||||||
|
|
||||||
// Telemetry Properties
|
// Telemetry Properties
|
||||||
|
|
||||||
// Modal Dialogs:
|
// Modal Dialogs:
|
||||||
@@ -42,3 +41,21 @@ export const Accounts = 'Accounts';
|
|||||||
export const FireWallRule = 'FirewallRule';
|
export const FireWallRule = 'FirewallRule';
|
||||||
export const AutoOAuth = 'AutoOAuth';
|
export const AutoOAuth = 'AutoOAuth';
|
||||||
export const AddNewDashboardTab = 'AddNewDashboardTab';
|
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';
|
'use strict';
|
||||||
import * as crypto from 'crypto';
|
|
||||||
import * as os from 'os';
|
|
||||||
import { ITelemetryService, ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
|
import { ITelemetryService, ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
|
||||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||||
import { warn } from 'sql/base/common/log';
|
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 {
|
export interface IConnectionTelemetryData extends ITelemetryData {
|
||||||
provider?: string;
|
provider?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import * as sqlops from 'sqlops';
|
|||||||
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||||
import { IErrorMessageService } from 'sql/parts/connection/common/connectionManagement';
|
import { IErrorMessageService } from 'sql/parts/connection/common/connectionManagement';
|
||||||
import { FirewallRuleDialog } from 'sql/parts/accountManagement/firewallRuleDialog/firewallRuleDialog';
|
import { FirewallRuleDialog } from 'sql/parts/accountManagement/firewallRuleDialog/firewallRuleDialog';
|
||||||
import { IAccountManagementService } from 'sql/services/accountManagement/interfaces';
|
import { IAccountManagementService, AzureResource } from 'sql/services/accountManagement/interfaces';
|
||||||
import { IResourceProviderService } from 'sql/parts/accountManagement/common/interfaces';
|
import { IResourceProviderService } from 'sql/parts/accountManagement/common/interfaces';
|
||||||
import { Deferred } from 'sql/base/common/promise';
|
import { Deferred } from 'sql/base/common/promise';
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ export class FirewallRuleDialogController {
|
|||||||
private handleOnCreateFirewallRule(): void {
|
private handleOnCreateFirewallRule(): void {
|
||||||
let resourceProviderId = this._resourceProviderId;
|
let resourceProviderId = this._resourceProviderId;
|
||||||
|
|
||||||
this._accountManagementService.getSecurityToken(this._firewallRuleDialog.viewModel.selectedAccount).then(tokenMappings => {
|
this._accountManagementService.getSecurityToken(this._firewallRuleDialog.viewModel.selectedAccount, AzureResource.ResourceManagement).then(tokenMappings => {
|
||||||
let firewallRuleInfo: sqlops.FirewallRuleInfo = {
|
let firewallRuleInfo: sqlops.FirewallRuleInfo = {
|
||||||
startIpAddress: this._firewallRuleDialog.viewModel.isIPAddressSelected ? this._firewallRuleDialog.viewModel.defaultIPAddress : this._firewallRuleDialog.viewModel.fromSubnetIPRange,
|
startIpAddress: this._firewallRuleDialog.viewModel.isIPAddressSelected ? this._firewallRuleDialog.viewModel.defaultIPAddress : this._firewallRuleDialog.viewModel.fromSubnetIPRange,
|
||||||
endIpAddress: this._firewallRuleDialog.viewModel.isIPAddressSelected ? this._firewallRuleDialog.viewModel.defaultIPAddress : this._firewallRuleDialog.viewModel.toSubnetIPRange,
|
endIpAddress: this._firewallRuleDialog.viewModel.isIPAddressSelected ? this._firewallRuleDialog.viewModel.defaultIPAddress : this._firewallRuleDialog.viewModel.toSubnetIPRange,
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export class CommandLineService implements ICommandLineProcessing {
|
|||||||
// prompt the user for a new connection on startup if no profiles are registered
|
// prompt the user for a new connection on startup if no profiles are registered
|
||||||
this._connectionManagementService.showConnectionDialog();
|
this._connectionManagementService.showConnectionDialog();
|
||||||
} else if (this._connectionProfile) {
|
} else if (this._connectionProfile) {
|
||||||
this._connectionManagementService.connectIfNotConnected(this._connectionProfile, 'connection')
|
this._connectionManagementService.connectIfNotConnected(this._connectionProfile, 'connection', true)
|
||||||
.then(result => TaskUtilities.newQuery(this._connectionProfile,
|
.then(result => TaskUtilities.newQuery(this._connectionProfile,
|
||||||
this._connectionManagementService,
|
this._connectionManagementService,
|
||||||
this._queryEditorService,
|
this._queryEditorService,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { QueryPlanInput } from 'sql/parts/queryPlan/queryPlanInput';
|
|||||||
import { NotebookInput, NotebookInputModel, NotebookInputValidator } from 'sql/parts/notebook/notebookInput';
|
import { NotebookInput, NotebookInputModel, NotebookInputValidator } from 'sql/parts/notebook/notebookInput';
|
||||||
import { Extensions, INotebookProviderRegistry } from 'sql/services/notebook/notebookRegistry';
|
import { Extensions, INotebookProviderRegistry } from 'sql/services/notebook/notebookRegistry';
|
||||||
import { DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService';
|
import { DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService';
|
||||||
|
import { getProviderForFileName } from 'sql/parts/notebook/notebookUtils';
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
@@ -183,17 +184,6 @@ function getNotebookFileExtensions() {
|
|||||||
return notebookRegistry.getSupportedFileExtensions();
|
return notebookRegistry.getSupportedFileExtensions();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProviderForFileName(fileName: string) {
|
|
||||||
let fileExt = path.extname(fileName);
|
|
||||||
if (fileExt && fileExt.startsWith('.')) {
|
|
||||||
fileExt = fileExt.slice(1,fileExt.length);
|
|
||||||
let notebookRegistry = Registry.as<INotebookProviderRegistry>(Extensions.NotebookProviderContribution);
|
|
||||||
return notebookRegistry.getProviderForFileType(fileExt);
|
|
||||||
}
|
|
||||||
return DEFAULT_NOTEBOOK_PROVIDER;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the given EditorInput is set to either undefined or sql mode
|
* Checks whether the given EditorInput is set to either undefined or sql mode
|
||||||
* @param input The EditorInput to check the mode of
|
* @param input The EditorInput to check the mode of
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export interface IConnectionManagementService {
|
|||||||
* otherwise tries to make a connection and returns the owner uri when connection is complete
|
* otherwise tries to make a connection and returns the owner uri when connection is complete
|
||||||
* The purpose is connection by default
|
* The purpose is connection by default
|
||||||
*/
|
*/
|
||||||
connectIfNotConnected(connection: IConnectionProfile, purpose?: 'dashboard' | 'insights' | 'connection'): Promise<string>;
|
connectIfNotConnected(connection: IConnectionProfile, purpose?: 'dashboard' | 'insights' | 'connection', saveConnection?: boolean): Promise<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the successful connection to MRU and send the connection error back to the connection handler for failed connections
|
* Adds the successful connection to MRU and send the connection error back to the connection handler for failed connections
|
||||||
|
|||||||
@@ -34,12 +34,13 @@ import { Deferred } from 'sql/base/common/promise';
|
|||||||
import { ConnectionOptionSpecialType } from 'sql/workbench/api/common/sqlExtHostTypes';
|
import { ConnectionOptionSpecialType } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||||
import { values } from 'sql/base/common/objects';
|
import { values } from 'sql/base/common/objects';
|
||||||
import { ConnectionProviderProperties, IConnectionProviderRegistry, Extensions as ConnectionProviderExtensions } from 'sql/workbench/parts/connection/common/connectionProviderExtension';
|
import { ConnectionProviderProperties, IConnectionProviderRegistry, Extensions as ConnectionProviderExtensions } from 'sql/workbench/parts/connection/common/connectionProviderExtension';
|
||||||
|
import { IAccountManagementService, AzureResource } from 'sql/services/accountManagement/interfaces';
|
||||||
|
|
||||||
import * as sqlops from 'sqlops';
|
import * as sqlops from 'sqlops';
|
||||||
|
|
||||||
import * as nls from 'vs/nls';
|
import * as nls from 'vs/nls';
|
||||||
import * as errors from 'vs/base/common/errors';
|
import * as errors from 'vs/base/common/errors';
|
||||||
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
|
import { Disposable } from 'vs/base/common/lifecycle';
|
||||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||||
import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
||||||
import * as platform from 'vs/platform/registry/common/platform';
|
import * as platform from 'vs/platform/registry/common/platform';
|
||||||
@@ -58,7 +59,6 @@ import * as statusbar from 'vs/workbench/browser/parts/statusbar/statusbar';
|
|||||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||||
import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar';
|
import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar';
|
||||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||||
import { EditorGroup } from 'vs/workbench/common/editor/editorGroup';
|
|
||||||
|
|
||||||
export class ConnectionManagementService extends Disposable implements IConnectionManagementService {
|
export class ConnectionManagementService extends Disposable implements IConnectionManagementService {
|
||||||
|
|
||||||
@@ -100,7 +100,8 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
|||||||
@IStatusbarService private _statusBarService: IStatusbarService,
|
@IStatusbarService private _statusBarService: IStatusbarService,
|
||||||
@IResourceProviderService private _resourceProviderService: IResourceProviderService,
|
@IResourceProviderService private _resourceProviderService: IResourceProviderService,
|
||||||
@IViewletService private _viewletService: IViewletService,
|
@IViewletService private _viewletService: IViewletService,
|
||||||
@IAngularEventingService private _angularEventing: IAngularEventingService
|
@IAngularEventingService private _angularEventing: IAngularEventingService,
|
||||||
|
@IAccountManagementService private _accountManagementService: IAccountManagementService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
if (this._instantiationService) {
|
if (this._instantiationService) {
|
||||||
@@ -248,7 +249,8 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
|||||||
* Load the password for the profile
|
* Load the password for the profile
|
||||||
* @param connectionProfile Connection Profile
|
* @param connectionProfile Connection Profile
|
||||||
*/
|
*/
|
||||||
public addSavedPassword(connectionProfile: IConnectionProfile): Promise<IConnectionProfile> {
|
public async addSavedPassword(connectionProfile: IConnectionProfile): Promise<IConnectionProfile> {
|
||||||
|
await this.fillInAzureTokenIfNeeded(connectionProfile);
|
||||||
return this._connectionStore.addSavedPassword(connectionProfile).then(result => result.profile);
|
return this._connectionStore.addSavedPassword(connectionProfile).then(result => result.profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +276,7 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
|||||||
let self = this;
|
let self = this;
|
||||||
return new Promise<IConnectionResult>((resolve, reject) => {
|
return new Promise<IConnectionResult>((resolve, reject) => {
|
||||||
// Load the password if it's not already loaded
|
// Load the password if it's not already loaded
|
||||||
self._connectionStore.addSavedPassword(connection).then(result => {
|
self._connectionStore.addSavedPassword(connection).then(async result => {
|
||||||
let newConnection = result.profile;
|
let newConnection = result.profile;
|
||||||
let foundPassword = result.savedCred;
|
let foundPassword = result.savedCred;
|
||||||
|
|
||||||
@@ -286,8 +288,12 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
|||||||
foundPassword = true;
|
foundPassword = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fill in the Azure account token if needed and open the connection dialog if it fails
|
||||||
|
let tokenFillSuccess = await self.fillInAzureTokenIfNeeded(newConnection);
|
||||||
|
|
||||||
// If the password is required and still not loaded show the dialog
|
// If the password is required and still not loaded show the dialog
|
||||||
if (!foundPassword && self._connectionStore.isPasswordRequired(newConnection) && !newConnection.password) {
|
if ((!foundPassword && self._connectionStore.isPasswordRequired(newConnection) && !newConnection.password) || !tokenFillSuccess) {
|
||||||
resolve(self.showConnectionDialogOnError(connection, owner, { connected: false, errorMessage: undefined, callStack: undefined, errorCode: undefined }, options));
|
resolve(self.showConnectionDialogOnError(connection, owner, { connected: false, errorMessage: undefined, callStack: undefined, errorCode: undefined }, options));
|
||||||
} else {
|
} else {
|
||||||
// Try to connect
|
// Try to connect
|
||||||
@@ -374,14 +380,14 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
|||||||
* otherwise tries to make a connection and returns the owner uri when connection is complete
|
* otherwise tries to make a connection and returns the owner uri when connection is complete
|
||||||
* The purpose is connection by default
|
* The purpose is connection by default
|
||||||
*/
|
*/
|
||||||
public connectIfNotConnected(connection: IConnectionProfile, purpose?: 'dashboard' | 'insights' | 'connection'): Promise<string> {
|
public connectIfNotConnected(connection: IConnectionProfile, purpose?: 'dashboard' | 'insights' | 'connection', saveConnection: boolean = false): Promise<string> {
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
let ownerUri: string = Utils.generateUri(connection, purpose);
|
let ownerUri: string = Utils.generateUri(connection, purpose);
|
||||||
if (this._connectionStatusManager.isConnected(ownerUri)) {
|
if (this._connectionStatusManager.isConnected(ownerUri)) {
|
||||||
resolve(this._connectionStatusManager.getOriginalOwnerUri(ownerUri));
|
resolve(this._connectionStatusManager.getOriginalOwnerUri(ownerUri));
|
||||||
} else {
|
} else {
|
||||||
const options: IConnectionCompletionOptions = {
|
const options: IConnectionCompletionOptions = {
|
||||||
saveTheConnection: false,
|
saveTheConnection: saveConnection,
|
||||||
showConnectionDialogOnError: true,
|
showConnectionDialogOnError: true,
|
||||||
showDashboard: purpose === 'dashboard',
|
showDashboard: purpose === 'dashboard',
|
||||||
params: undefined,
|
params: undefined,
|
||||||
@@ -449,10 +455,14 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
|||||||
showFirewallRuleOnError: true
|
showFirewallRuleOnError: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return new Promise<IConnectionResult>((resolve, reject) => {
|
return new Promise<IConnectionResult>(async (resolve, reject) => {
|
||||||
if (callbacks.onConnectStart) {
|
if (callbacks.onConnectStart) {
|
||||||
callbacks.onConnectStart();
|
callbacks.onConnectStart();
|
||||||
}
|
}
|
||||||
|
let tokenFillSuccess = await this.fillInAzureTokenIfNeeded(connection);
|
||||||
|
if (!tokenFillSuccess) {
|
||||||
|
throw new Error(nls.localize('connection.noAzureAccount', 'Failed to get Azure account token for connection'));
|
||||||
|
}
|
||||||
this.createNewConnection(uri, connection).then(connectionResult => {
|
this.createNewConnection(uri, connection).then(connectionResult => {
|
||||||
if (connectionResult && connectionResult.connected) {
|
if (connectionResult && connectionResult.connected) {
|
||||||
if (callbacks.onConnectSuccess) {
|
if (callbacks.onConnectSuccess) {
|
||||||
@@ -743,8 +753,44 @@ export class ConnectionManagementService extends Disposable implements IConnecti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async fillInAzureTokenIfNeeded(connection: IConnectionProfile): Promise<boolean> {
|
||||||
|
if (connection.authenticationType !== Constants.azureMFA || connection.options['azureAccountToken']) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let accounts = await this._accountManagementService.getAccountsForProvider('azurePublicCloud');
|
||||||
|
if (accounts && accounts.length > 0) {
|
||||||
|
let account = accounts.find(account => account.key.accountId === connection.userName);
|
||||||
|
if (account) {
|
||||||
|
if (account.isStale) {
|
||||||
|
try {
|
||||||
|
account = await this._accountManagementService.refreshAccount(account);
|
||||||
|
} catch {
|
||||||
|
// refreshAccount throws an error if the user cancels the dialog
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Request Senders
|
// Request Senders
|
||||||
private sendConnectRequest(connection: IConnectionProfile, uri: string): Thenable<boolean> {
|
private async sendConnectRequest(connection: IConnectionProfile, uri: string): Promise<boolean> {
|
||||||
let connectionInfo = Object.assign({}, {
|
let connectionInfo = Object.assign({}, {
|
||||||
options: connection.options
|
options: connection.options
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export class ConnectionProfile extends ProviderConnectionInfo implements interfa
|
|||||||
this.savePassword = model.savePassword;
|
this.savePassword = model.savePassword;
|
||||||
this.saveProfile = model.saveProfile;
|
this.saveProfile = model.saveProfile;
|
||||||
this._id = model.id;
|
this._id = model.id;
|
||||||
|
this.azureTenantId = model.azureTenantId;
|
||||||
} else {
|
} else {
|
||||||
//Default for a new connection
|
//Default for a new connection
|
||||||
this.savePassword = false;
|
this.savePassword = false;
|
||||||
@@ -84,6 +85,14 @@ export class ConnectionProfile extends ProviderConnectionInfo implements interfa
|
|||||||
this._id = value;
|
this._id = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get azureTenantId(): string {
|
||||||
|
return this.options['azureTenantId'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public set azureTenantId(value: string) {
|
||||||
|
this.options['azureTenantId'] = value;
|
||||||
|
}
|
||||||
|
|
||||||
public get groupFullName(): string {
|
public get groupFullName(): string {
|
||||||
return this._groupName;
|
return this._groupName;
|
||||||
}
|
}
|
||||||
@@ -159,7 +168,8 @@ export class ConnectionProfile extends ProviderConnectionInfo implements interfa
|
|||||||
userName: this.userName,
|
userName: this.userName,
|
||||||
options: this.options,
|
options: this.options,
|
||||||
saveProfile: this.saveProfile,
|
saveProfile: this.saveProfile,
|
||||||
id: this.id
|
id: this.id,
|
||||||
|
azureTenantId: this.azureTenantId
|
||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -33,4 +33,5 @@ export const passwordChars = '***************';
|
|||||||
/* authentication types */
|
/* authentication types */
|
||||||
export const sqlLogin = 'SqlLogin';
|
export const sqlLogin = 'SqlLogin';
|
||||||
export const integrated = 'Integrated';
|
export const integrated = 'Integrated';
|
||||||
|
export const azureMFA = 'AzureMFA';
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import { Dropdown } from 'sql/base/browser/ui/editableDropdown/dropdown';
|
|||||||
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
|
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
|
||||||
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
|
import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService';
|
||||||
import { ConnectionProfile } from '../common/connectionProfile';
|
import { ConnectionProfile } from '../common/connectionProfile';
|
||||||
|
import * as styler from 'sql/common/theme/styler';
|
||||||
|
import { IAccountManagementService } from 'sql/services/accountManagement/interfaces';
|
||||||
|
|
||||||
import * as sqlops from 'sqlops';
|
import * as sqlops from 'sqlops';
|
||||||
|
|
||||||
@@ -30,7 +32,6 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView
|
|||||||
import { localize } from 'vs/nls';
|
import { localize } from 'vs/nls';
|
||||||
import * as DOM from 'vs/base/browser/dom';
|
import * as DOM from 'vs/base/browser/dom';
|
||||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||||
import * as styler from 'vs/platform/theme/common/styler';
|
|
||||||
import { OS, OperatingSystem } from 'vs/base/common/platform';
|
import { OS, OperatingSystem } from 'vs/base/common/platform';
|
||||||
import { Builder, $ } from 'vs/base/browser/builder';
|
import { Builder, $ } from 'vs/base/browser/builder';
|
||||||
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
|
import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||||
@@ -50,6 +51,13 @@ export class ConnectionWidget {
|
|||||||
private _passwordInputBox: InputBox;
|
private _passwordInputBox: InputBox;
|
||||||
private _password: string;
|
private _password: string;
|
||||||
private _rememberPasswordCheckBox: Checkbox;
|
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 _advancedButton: Button;
|
||||||
private _callbacks: IConnectionComponentCallbacks;
|
private _callbacks: IConnectionComponentCallbacks;
|
||||||
private _authTypeSelectBox: SelectBox;
|
private _authTypeSelectBox: SelectBox;
|
||||||
@@ -59,7 +67,7 @@ export class ConnectionWidget {
|
|||||||
private _focusedBeforeHandleOnConnection: HTMLElement;
|
private _focusedBeforeHandleOnConnection: HTMLElement;
|
||||||
private _providerName: string;
|
private _providerName: string;
|
||||||
private _authTypeMap: { [providerName: string]: AuthenticationType[] } = {
|
private _authTypeMap: { [providerName: string]: AuthenticationType[] } = {
|
||||||
[Constants.mssqlProviderName]: [new AuthenticationType(Constants.integrated, false), new AuthenticationType(Constants.sqlLogin, true)]
|
[Constants.mssqlProviderName]: [AuthenticationType.SqlLogin, AuthenticationType.Integrated, AuthenticationType.AzureMFA]
|
||||||
};
|
};
|
||||||
private _saveProfile: boolean;
|
private _saveProfile: boolean;
|
||||||
private _databaseDropdownExpanded: boolean = false;
|
private _databaseDropdownExpanded: boolean = false;
|
||||||
@@ -96,7 +104,8 @@ export class ConnectionWidget {
|
|||||||
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
@IConnectionManagementService private _connectionManagementService: IConnectionManagementService,
|
||||||
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService,
|
@ICapabilitiesService private _capabilitiesService: ICapabilitiesService,
|
||||||
@IClipboardService private _clipboardService: IClipboardService,
|
@IClipboardService private _clipboardService: IClipboardService,
|
||||||
@IConfigurationService private _configurationService: IConfigurationService
|
@IConfigurationService private _configurationService: IConfigurationService,
|
||||||
|
@IAccountManagementService private _accountManagementService: IAccountManagementService
|
||||||
) {
|
) {
|
||||||
this._callbacks = callbacks;
|
this._callbacks = callbacks;
|
||||||
this._toDispose = [];
|
this._toDispose = [];
|
||||||
@@ -109,9 +118,9 @@ export class ConnectionWidget {
|
|||||||
var authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
|
var authTypeOption = this._optionsMaps[ConnectionOptionSpecialType.authType];
|
||||||
if (authTypeOption) {
|
if (authTypeOption) {
|
||||||
if (OS === OperatingSystem.Windows) {
|
if (OS === OperatingSystem.Windows) {
|
||||||
authTypeOption.defaultValue = this.getAuthTypeDisplayName(Constants.integrated);
|
authTypeOption.defaultValue = this.getAuthTypeDisplayName(AuthenticationType.Integrated);
|
||||||
} else {
|
} else {
|
||||||
authTypeOption.defaultValue = this.getAuthTypeDisplayName(Constants.sqlLogin);
|
authTypeOption.defaultValue = this.getAuthTypeDisplayName(AuthenticationType.SqlLogin);
|
||||||
}
|
}
|
||||||
this._authTypeSelectBox = new SelectBox(authTypeOption.categoryValues.map(c => c.displayName), authTypeOption.defaultValue, this._contextViewService, undefined, { ariaLabel: authTypeOption.displayName });
|
this._authTypeSelectBox = new SelectBox(authTypeOption.categoryValues.map(c => c.displayName), authTypeOption.defaultValue, this._contextViewService, undefined, { ariaLabel: authTypeOption.displayName });
|
||||||
}
|
}
|
||||||
@@ -182,7 +191,7 @@ export class ConnectionWidget {
|
|||||||
// Username
|
// Username
|
||||||
let self = this;
|
let self = this;
|
||||||
let userNameOption = this._optionsMaps[ConnectionOptionSpecialType.userName];
|
let userNameOption = this._optionsMaps[ConnectionOptionSpecialType.userName];
|
||||||
let userNameBuilder = DialogHelper.appendRow(this._tableContainer, userNameOption.displayName, 'connection-label', 'connection-input');
|
let userNameBuilder = DialogHelper.appendRow(this._tableContainer, userNameOption.displayName, 'connection-label', 'connection-input', 'username-password-row');
|
||||||
this._userNameInputBox = new InputBox(userNameBuilder.getHTMLElement(), this._contextViewService, {
|
this._userNameInputBox = new InputBox(userNameBuilder.getHTMLElement(), this._contextViewService, {
|
||||||
validationOptions: {
|
validationOptions: {
|
||||||
validation: (value: string) => self.validateUsername(value, userNameOption.isRequired) ? ({ type: MessageType.ERROR, content: localize('connectionWidget.missingRequireField', '{0} is required.', userNameOption.displayName) }) : null
|
validation: (value: string) => self.validateUsername(value, userNameOption.isRequired) ? ({ type: MessageType.ERROR, content: localize('connectionWidget.missingRequireField', '{0} is required.', userNameOption.displayName) }) : null
|
||||||
@@ -191,14 +200,28 @@ export class ConnectionWidget {
|
|||||||
});
|
});
|
||||||
// Password
|
// Password
|
||||||
let passwordOption = this._optionsMaps[ConnectionOptionSpecialType.password];
|
let passwordOption = this._optionsMaps[ConnectionOptionSpecialType.password];
|
||||||
let passwordBuilder = DialogHelper.appendRow(this._tableContainer, passwordOption.displayName, 'connection-label', 'connection-input');
|
let passwordBuilder = DialogHelper.appendRow(this._tableContainer, passwordOption.displayName, 'connection-label', 'connection-input', 'username-password-row');
|
||||||
this._passwordInputBox = new InputBox(passwordBuilder.getHTMLElement(), this._contextViewService, { ariaLabel: passwordOption.displayName });
|
this._passwordInputBox = new InputBox(passwordBuilder.getHTMLElement(), this._contextViewService, { ariaLabel: passwordOption.displayName });
|
||||||
this._passwordInputBox.inputElement.type = 'password';
|
this._passwordInputBox.inputElement.type = 'password';
|
||||||
this._password = '';
|
this._password = '';
|
||||||
|
|
||||||
// Remember password
|
// Remember password
|
||||||
let rememberPasswordLabel = localize('rememberPassword', 'Remember password');
|
let rememberPasswordLabel = localize('rememberPassword', 'Remember password');
|
||||||
this._rememberPasswordCheckBox = this.appendCheckbox(this._tableContainer, rememberPasswordLabel, 'connection-checkbox', 'connection-input', false);
|
this._rememberPasswordCheckBox = this.appendCheckbox(this._tableContainer, rememberPasswordLabel, 'connection-checkbox', 'connection-input', 'username-password-row', false);
|
||||||
|
|
||||||
|
// Azure account picker
|
||||||
|
let accountLabel = localize('connection.azureAccountDropdownLabel', 'Account');
|
||||||
|
let accountDropdownBuilder = DialogHelper.appendRow(this._tableContainer, accountLabel, 'connection-label', 'connection-input', 'azure-account-row');
|
||||||
|
this._azureAccountDropdown = new SelectBox([], undefined, this._contextViewService, accountDropdownBuilder.getContainer(), { ariaLabel: accountLabel });
|
||||||
|
DialogHelper.appendInputSelectBox(accountDropdownBuilder, this._azureAccountDropdown);
|
||||||
|
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
|
// Database
|
||||||
let databaseOption = this._optionsMaps[ConnectionOptionSpecialType.databaseName];
|
let databaseOption = this._optionsMaps[ConnectionOptionSpecialType.databaseName];
|
||||||
@@ -228,7 +251,7 @@ export class ConnectionWidget {
|
|||||||
|
|
||||||
private validateUsername(value: string, isOptionRequired: boolean): boolean {
|
private validateUsername(value: string, isOptionRequired: boolean): boolean {
|
||||||
let currentAuthType = this._authTypeSelectBox ? this.getMatchingAuthType(this._authTypeSelectBox.value) : undefined;
|
let currentAuthType = this._authTypeSelectBox ? this.getMatchingAuthType(this._authTypeSelectBox.value) : undefined;
|
||||||
if (!currentAuthType || currentAuthType.showUsernameAndPassword) {
|
if (!currentAuthType || currentAuthType === AuthenticationType.SqlLogin) {
|
||||||
if (!value && isOptionRequired) {
|
if (!value && isOptionRequired) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -254,9 +277,9 @@ export class ConnectionWidget {
|
|||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
private appendCheckbox(container: Builder, label: string, checkboxClass: string, cellContainerClass: string, isChecked: boolean): Checkbox {
|
private appendCheckbox(container: Builder, label: string, checkboxClass: string, cellContainerClass: string, rowContainerClass: string, isChecked: boolean): Checkbox {
|
||||||
let checkbox: Checkbox;
|
let checkbox: Checkbox;
|
||||||
container.element('tr', {}, (rowContainer) => {
|
container.element('tr', { class: rowContainerClass }, (rowContainer) => {
|
||||||
rowContainer.element('td');
|
rowContainer.element('td');
|
||||||
rowContainer.element('td', { class: cellContainerClass }, (inputCellContainer) => {
|
rowContainer.element('td', { class: cellContainerClass }, (inputCellContainer) => {
|
||||||
checkbox = new Checkbox(inputCellContainer.getHTMLElement(), { label, checked: isChecked, ariaLabel: label });
|
checkbox = new Checkbox(inputCellContainer.getHTMLElement(), { label, checked: isChecked, ariaLabel: label });
|
||||||
@@ -275,6 +298,7 @@ export class ConnectionWidget {
|
|||||||
this._toDispose.push(styler.attachSelectBoxStyler(this._serverGroupSelectBox, this._themeService));
|
this._toDispose.push(styler.attachSelectBoxStyler(this._serverGroupSelectBox, this._themeService));
|
||||||
this._toDispose.push(attachButtonStyler(this._advancedButton, this._themeService));
|
this._toDispose.push(attachButtonStyler(this._advancedButton, this._themeService));
|
||||||
this._toDispose.push(attachCheckboxStyler(this._rememberPasswordCheckBox, this._themeService));
|
this._toDispose.push(attachCheckboxStyler(this._rememberPasswordCheckBox, this._themeService));
|
||||||
|
this._toDispose.push(styler.attachSelectBoxStyler(this._azureAccountDropdown, this._themeService));
|
||||||
|
|
||||||
if (this._authTypeSelectBox) {
|
if (this._authTypeSelectBox) {
|
||||||
// Theme styler
|
// Theme styler
|
||||||
@@ -285,6 +309,30 @@ export class ConnectionWidget {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._azureAccountDropdown) {
|
||||||
|
this._toDispose.push(styler.attachSelectBoxStyler(this._azureAccountDropdown, this._themeService));
|
||||||
|
this._toDispose.push(this._azureAccountDropdown.onDidSelect(() => {
|
||||||
|
this.onAzureAccountSelected();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
if (account) {
|
||||||
|
await this._accountManagementService.refreshAccount(account);
|
||||||
|
this.fillInAzureAccountOptions();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
this._toDispose.push(this._serverGroupSelectBox.onDidSelect(selectedGroup => {
|
this._toDispose.push(this._serverGroupSelectBox.onDidSelect(selectedGroup => {
|
||||||
this.onGroupSelected(selectedGroup.selected);
|
this.onGroupSelected(selectedGroup.selected);
|
||||||
}));
|
}));
|
||||||
@@ -342,7 +390,7 @@ export class ConnectionWidget {
|
|||||||
private setConnectButton(): void {
|
private setConnectButton(): void {
|
||||||
let showUsernameAndPassword: boolean = true;
|
let showUsernameAndPassword: boolean = true;
|
||||||
if (this.authType) {
|
if (this.authType) {
|
||||||
showUsernameAndPassword = this.authType.showUsernameAndPassword;
|
showUsernameAndPassword = this.authType === AuthenticationType.SqlLogin;
|
||||||
}
|
}
|
||||||
showUsernameAndPassword ? this._callbacks.onSetConnectButton(!!this.serverName && !!this.userName) :
|
showUsernameAndPassword ? this._callbacks.onSetConnectButton(!!this.serverName && !!this.userName) :
|
||||||
this._callbacks.onSetConnectButton(!!this.serverName);
|
this._callbacks.onSetConnectButton(!!this.serverName);
|
||||||
@@ -350,7 +398,7 @@ export class ConnectionWidget {
|
|||||||
|
|
||||||
private onAuthTypeSelected(selectedAuthType: string) {
|
private onAuthTypeSelected(selectedAuthType: string) {
|
||||||
let currentAuthType = this.getMatchingAuthType(selectedAuthType);
|
let currentAuthType = this.getMatchingAuthType(selectedAuthType);
|
||||||
if (!currentAuthType.showUsernameAndPassword) {
|
if (currentAuthType !== AuthenticationType.SqlLogin) {
|
||||||
this._userNameInputBox.disable();
|
this._userNameInputBox.disable();
|
||||||
this._passwordInputBox.disable();
|
this._passwordInputBox.disable();
|
||||||
this._userNameInputBox.hideMessage();
|
this._userNameInputBox.hideMessage();
|
||||||
@@ -366,6 +414,96 @@ export class ConnectionWidget {
|
|||||||
this._passwordInputBox.enable();
|
this._passwordInputBox.enable();
|
||||||
this._rememberPasswordCheckBox.enabled = true;
|
this._rememberPasswordCheckBox.enabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentAuthType === AuthenticationType.AzureMFA) {
|
||||||
|
this.fillInAzureAccountOptions();
|
||||||
|
this._azureAccountDropdown.enable();
|
||||||
|
let tableContainer = this._tableContainer.getContainer();
|
||||||
|
tableContainer.classList.add('hide-username-password');
|
||||||
|
tableContainer.classList.remove('hide-azure-accounts');
|
||||||
|
} else {
|
||||||
|
this._azureAccountDropdown.disable();
|
||||||
|
let tableContainer = this._tableContainer.getContainer();
|
||||||
|
tableContainer.classList.remove('hide-username-password');
|
||||||
|
tableContainer.classList.add('hide-azure-accounts');
|
||||||
|
this._azureAccountDropdown.hideMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fillInAzureAccountOptions(): Promise<void> {
|
||||||
|
let oldSelection = this._azureAccountDropdown.value;
|
||||||
|
this._azureAccountList = await this._accountManagementService.getAccountsForProvider(this._azureProviderId);
|
||||||
|
let accountDropdownOptions = this._azureAccountList.map(account => account.key.accountId);
|
||||||
|
if (accountDropdownOptions.length === 0) {
|
||||||
|
// If there are no accounts add a blank option so that add account isn't automatically selected
|
||||||
|
accountDropdownOptions.unshift('');
|
||||||
|
}
|
||||||
|
accountDropdownOptions.push(this._addAzureAccountMessage);
|
||||||
|
this._azureAccountDropdown.setOptions(accountDropdownOptions);
|
||||||
|
this._azureAccountDropdown.selectWithOptionName(oldSelection);
|
||||||
|
await this.onAzureAccountSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateRefreshCredentialsLink(): Promise<void> {
|
||||||
|
let chosenAccount = this._azureAccountList.find(account => account.key.accountId === this._azureAccountDropdown.value);
|
||||||
|
if (chosenAccount && chosenAccount.isStale) {
|
||||||
|
this._tableContainer.getContainer().classList.remove('hide-refresh-link');
|
||||||
|
} else {
|
||||||
|
this._tableContainer.getContainer().classList.add('hide-refresh-link');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Open the add account dialog if needed, then select the added account
|
||||||
|
if (this._azureAccountDropdown.value === this._addAzureAccountMessage) {
|
||||||
|
let oldAccountIds = this._azureAccountList.map(account => account.key.accountId);
|
||||||
|
await this._accountManagementService.addAccount(this._azureProviderId);
|
||||||
|
|
||||||
|
// Refresh the dropdown's list to include the added account
|
||||||
|
await this.fillInAzureAccountOptions();
|
||||||
|
|
||||||
|
// If a new account was added find it and select it, otherwise select the first account
|
||||||
|
let newAccount = this._azureAccountList.find(option => !oldAccountIds.some(oldId => oldId === option.key.accountId));
|
||||||
|
if (newAccount) {
|
||||||
|
this._azureAccountDropdown.selectWithOptionName(newAccount.key.accountId);
|
||||||
|
} else {
|
||||||
|
this._azureAccountDropdown.select(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
private serverNameChanged(serverName: string) {
|
||||||
@@ -407,6 +545,7 @@ export class ConnectionWidget {
|
|||||||
private clearValidationMessages(): void {
|
private clearValidationMessages(): void {
|
||||||
this._serverNameInputBox.hideMessage();
|
this._serverNameInputBox.hideMessage();
|
||||||
this._userNameInputBox.hideMessage();
|
this._userNameInputBox.hideMessage();
|
||||||
|
this._azureAccountDropdown.hideMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getModelValue(value: string): string {
|
private getModelValue(value: string): string {
|
||||||
@@ -422,6 +561,7 @@ export class ConnectionWidget {
|
|||||||
this._passwordInputBox.value = connectionInfo.password ? Constants.passwordChars : '';
|
this._passwordInputBox.value = connectionInfo.password ? Constants.passwordChars : '';
|
||||||
this._password = this.getModelValue(connectionInfo.password);
|
this._password = this.getModelValue(connectionInfo.password);
|
||||||
this._saveProfile = connectionInfo.saveProfile;
|
this._saveProfile = connectionInfo.saveProfile;
|
||||||
|
this._azureTenantId = connectionInfo.azureTenantId;
|
||||||
let groupName: string;
|
let groupName: string;
|
||||||
if (this._saveProfile) {
|
if (this._saveProfile) {
|
||||||
if (!connectionInfo.groupFullName) {
|
if (!connectionInfo.groupFullName) {
|
||||||
@@ -449,8 +589,28 @@ export class ConnectionWidget {
|
|||||||
|
|
||||||
if (this._authTypeSelectBox) {
|
if (this._authTypeSelectBox) {
|
||||||
this.onAuthTypeSelected(this._authTypeSelectBox.value);
|
this.onAuthTypeSelected(this._authTypeSelectBox.value);
|
||||||
|
} else {
|
||||||
|
let tableContainerElement = this._tableContainer.getContainer();
|
||||||
|
tableContainerElement.classList.remove('hide-username-password');
|
||||||
|
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 -
|
// Disable connect button if -
|
||||||
// 1. Authentication type is SQL Login and no username is provided
|
// 1. Authentication type is SQL Login and no username is provided
|
||||||
// 2. No server name is provided
|
// 2. No server name is provided
|
||||||
@@ -513,7 +673,7 @@ export class ConnectionWidget {
|
|||||||
currentAuthType = this.getMatchingAuthType(this._authTypeSelectBox.value);
|
currentAuthType = this.getMatchingAuthType(this._authTypeSelectBox.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentAuthType || currentAuthType.showUsernameAndPassword) {
|
if (!currentAuthType || currentAuthType === AuthenticationType.SqlLogin) {
|
||||||
this._userNameInputBox.enable();
|
this._userNameInputBox.enable();
|
||||||
this._passwordInputBox.enable();
|
this._passwordInputBox.enable();
|
||||||
this._rememberPasswordCheckBox.enabled = true;
|
this._rememberPasswordCheckBox.enabled = true;
|
||||||
@@ -537,7 +697,7 @@ export class ConnectionWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get userName(): string {
|
public get userName(): string {
|
||||||
return this._userNameInputBox.value;
|
return this.authenticationType === AuthenticationType.AzureMFA ? this._azureAccountDropdown.value : this._userNameInputBox.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get password(): string {
|
public get password(): string {
|
||||||
@@ -548,6 +708,27 @@ export class ConnectionWidget {
|
|||||||
return this._authTypeSelectBox ? this.getAuthTypeName(this._authTypeSelectBox.value) : undefined;
|
return this._authTypeSelectBox ? this.getAuthTypeName(this._authTypeSelectBox.value) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private validateAzureAccountSelection(showMessage: boolean = true): boolean {
|
||||||
|
if (this.authType !== AuthenticationType.AzureMFA) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let selected = this._azureAccountDropdown.value;
|
||||||
|
if (selected === '' || selected === this._addAzureAccountMessage) {
|
||||||
|
if (showMessage) {
|
||||||
|
this._azureAccountDropdown.showMessage({
|
||||||
|
content: localize('connectionWidget.invalidAzureAccount', 'You must select an account'),
|
||||||
|
type: MessageType.ERROR
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
this._azureAccountDropdown.hideMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private validateInputs(): boolean {
|
private validateInputs(): boolean {
|
||||||
let isFocused = false;
|
let isFocused = false;
|
||||||
let validateServerName = this._serverNameInputBox.validate();
|
let validateServerName = this._serverNameInputBox.validate();
|
||||||
@@ -565,7 +746,12 @@ export class ConnectionWidget {
|
|||||||
this._passwordInputBox.focus();
|
this._passwordInputBox.focus();
|
||||||
isFocused = true;
|
isFocused = true;
|
||||||
}
|
}
|
||||||
return validateServerName && validateUserName && validatePassword;
|
let validateAzureAccount = this.validateAzureAccountSelection();
|
||||||
|
if (!validateAzureAccount && !isFocused) {
|
||||||
|
this._azureAccountDropdown.focus();
|
||||||
|
isFocused = true;
|
||||||
|
}
|
||||||
|
return validateServerName && validateUserName && validatePassword && validateAzureAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public connect(model: IConnectionProfile): boolean {
|
public connect(model: IConnectionProfile): boolean {
|
||||||
@@ -590,6 +776,9 @@ export class ConnectionWidget {
|
|||||||
model.saveProfile = true;
|
model.saveProfile = true;
|
||||||
model.groupId = this.findGroupId(model.groupFullName);
|
model.groupId = this.findGroupId(model.groupFullName);
|
||||||
}
|
}
|
||||||
|
if (this.authType === AuthenticationType.AzureMFA) {
|
||||||
|
model.azureTenantId = this._azureTenantId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return validInputs;
|
return validInputs;
|
||||||
}
|
}
|
||||||
@@ -613,7 +802,7 @@ export class ConnectionWidget {
|
|||||||
|
|
||||||
private getMatchingAuthType(displayName: string): AuthenticationType {
|
private getMatchingAuthType(displayName: string): AuthenticationType {
|
||||||
const authType = this._authTypeMap[this._providerName];
|
const authType = this._authTypeMap[this._providerName];
|
||||||
return authType ? authType.find(authType => this.getAuthTypeDisplayName(authType.name) === displayName) : undefined;
|
return authType ? authType.find(authType => this.getAuthTypeDisplayName(authType) === displayName) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDatabaseDropdown(): void {
|
public closeDatabaseDropdown(): void {
|
||||||
@@ -634,18 +823,14 @@ export class ConnectionWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private focusPasswordIfNeeded(): void {
|
private focusPasswordIfNeeded(): void {
|
||||||
if (this.authType && this.authType.showUsernameAndPassword && this.userName && !this.password) {
|
if (this.authType && this.authType === AuthenticationType.SqlLogin && this.userName && !this.password) {
|
||||||
this._passwordInputBox.focus();
|
this._passwordInputBox.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthenticationType {
|
enum AuthenticationType {
|
||||||
public name: string;
|
SqlLogin = 'SqlLogin',
|
||||||
public showUsernameAndPassword: boolean;
|
Integrated = 'Integrated',
|
||||||
|
AzureMFA = 'AzureMFA'
|
||||||
constructor(name: string, showUsernameAndPassword: boolean) {
|
|
||||||
this.name = name;
|
|
||||||
this.showUsernameAndPassword = showUsernameAndPassword;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -28,11 +28,12 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0px 11px;
|
margin: 0px 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-dialog .tabBody {
|
.connection-dialog .tabBody {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex: 1 1;
|
flex: 1 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection-recent, .connection-saved {
|
.connection-recent, .connection-saved {
|
||||||
@@ -114,4 +115,20 @@
|
|||||||
margin: 5px 0px;
|
margin: 5px 0px;
|
||||||
padding: 5px 15px;
|
padding: 5px 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hide-azure-accounts .azure-account-row {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-username-password .username-password-row {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-refresh-link .azure-account-row.refresh-credentials-link {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-azure-tenants .azure-tenant-row {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import { AlertsViewComponent } from 'sql/parts/jobManagement/views/alertsView.co
|
|||||||
import { OperatorsViewComponent } from 'sql/parts/jobManagement/views/operatorsView.component';
|
import { OperatorsViewComponent } from 'sql/parts/jobManagement/views/operatorsView.component';
|
||||||
import { ProxiesViewComponent } from 'sql/parts/jobManagement/views/proxiesView.component';
|
import { ProxiesViewComponent } from 'sql/parts/jobManagement/views/proxiesView.component';
|
||||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
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 {
|
export enum JobActions {
|
||||||
Run = 'run',
|
Run = 'run',
|
||||||
@@ -80,7 +83,8 @@ export class RunJobAction extends Action {
|
|||||||
constructor(
|
constructor(
|
||||||
@INotificationService private notificationService: INotificationService,
|
@INotificationService private notificationService: INotificationService,
|
||||||
@IJobManagementService private jobManagementService: IJobManagementService,
|
@IJobManagementService private jobManagementService: IJobManagementService,
|
||||||
@IInstantiationService private instantationService: IInstantiationService
|
@IInstantiationService private instantationService: IInstantiationService,
|
||||||
|
@ITelemetryService private telemetryService: ITelemetryService
|
||||||
) {
|
) {
|
||||||
super(RunJobAction.ID, RunJobAction.LABEL, 'runJobIcon');
|
super(RunJobAction.ID, RunJobAction.LABEL, 'runJobIcon');
|
||||||
}
|
}
|
||||||
@@ -89,6 +93,7 @@ export class RunJobAction extends Action {
|
|||||||
let jobName = context.agentJobInfo.name;
|
let jobName = context.agentJobInfo.name;
|
||||||
let ownerUri = context.ownerUri;
|
let ownerUri = context.ownerUri;
|
||||||
let refreshAction = this.instantationService.createInstance(JobsRefreshAction);
|
let refreshAction = this.instantationService.createInstance(JobsRefreshAction);
|
||||||
|
this.telemetryService.publicLog(TelemetryKeys.RunAgentJob);
|
||||||
return new TPromise<boolean>((resolve, reject) => {
|
return new TPromise<boolean>((resolve, reject) => {
|
||||||
this.jobManagementService.jobAction(ownerUri, jobName, JobActions.Run).then(result => {
|
this.jobManagementService.jobAction(ownerUri, jobName, JobActions.Run).then(result => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -118,7 +123,8 @@ export class StopJobAction extends Action {
|
|||||||
constructor(
|
constructor(
|
||||||
@INotificationService private notificationService: INotificationService,
|
@INotificationService private notificationService: INotificationService,
|
||||||
@IJobManagementService private jobManagementService: IJobManagementService,
|
@IJobManagementService private jobManagementService: IJobManagementService,
|
||||||
@IInstantiationService private instantationService: IInstantiationService
|
@IInstantiationService private instantationService: IInstantiationService,
|
||||||
|
@ITelemetryService private telemetryService: ITelemetryService
|
||||||
) {
|
) {
|
||||||
super(StopJobAction.ID, StopJobAction.LABEL, 'stopJobIcon');
|
super(StopJobAction.ID, StopJobAction.LABEL, 'stopJobIcon');
|
||||||
}
|
}
|
||||||
@@ -127,6 +133,7 @@ export class StopJobAction extends Action {
|
|||||||
let jobName = context.agentJobInfo.name;
|
let jobName = context.agentJobInfo.name;
|
||||||
let ownerUri = context.ownerUri;
|
let ownerUri = context.ownerUri;
|
||||||
let refreshAction = this.instantationService.createInstance(JobsRefreshAction);
|
let refreshAction = this.instantationService.createInstance(JobsRefreshAction);
|
||||||
|
this.telemetryService.publicLog(TelemetryKeys.StopAgentJob);
|
||||||
return new TPromise<boolean>((resolve, reject) => {
|
return new TPromise<boolean>((resolve, reject) => {
|
||||||
this.jobManagementService.jobAction(ownerUri, jobName, JobActions.Stop).then(result => {
|
this.jobManagementService.jobAction(ownerUri, jobName, JobActions.Stop).then(result => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -174,7 +181,8 @@ export class DeleteJobAction extends Action {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@INotificationService private _notificationService: INotificationService,
|
@INotificationService private _notificationService: INotificationService,
|
||||||
@IJobManagementService private _jobService: IJobManagementService
|
@IJobManagementService private _jobService: IJobManagementService,
|
||||||
|
@ITelemetryService private _telemetryService: ITelemetryService
|
||||||
) {
|
) {
|
||||||
super(DeleteJobAction.ID, DeleteJobAction.LABEL);
|
super(DeleteJobAction.ID, DeleteJobAction.LABEL);
|
||||||
}
|
}
|
||||||
@@ -188,6 +196,7 @@ export class DeleteJobAction extends Action {
|
|||||||
[{
|
[{
|
||||||
label: DeleteJobAction.LABEL,
|
label: DeleteJobAction.LABEL,
|
||||||
run: () => {
|
run: () => {
|
||||||
|
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentJob);
|
||||||
self._jobService.deleteJob(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
|
self._jobService.deleteJob(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
|
||||||
if (!result || !result.success) {
|
if (!result || !result.success) {
|
||||||
let errorMessage = nls.localize("jobaction.failedToDeleteJob", "Could not delete job '{0}'.\nError: {1}",
|
let errorMessage = nls.localize("jobaction.failedToDeleteJob", "Could not delete job '{0}'.\nError: {1}",
|
||||||
@@ -234,7 +243,8 @@ export class DeleteStepAction extends Action {
|
|||||||
constructor(
|
constructor(
|
||||||
@INotificationService private _notificationService: INotificationService,
|
@INotificationService private _notificationService: INotificationService,
|
||||||
@IJobManagementService private _jobService: IJobManagementService,
|
@IJobManagementService private _jobService: IJobManagementService,
|
||||||
@IInstantiationService private instantationService: IInstantiationService
|
@IInstantiationService private instantationService: IInstantiationService,
|
||||||
|
@ITelemetryService private _telemetryService: ITelemetryService
|
||||||
) {
|
) {
|
||||||
super(DeleteStepAction.ID, DeleteStepAction.LABEL);
|
super(DeleteStepAction.ID, DeleteStepAction.LABEL);
|
||||||
}
|
}
|
||||||
@@ -249,6 +259,7 @@ export class DeleteStepAction extends Action {
|
|||||||
[{
|
[{
|
||||||
label: DeleteStepAction.LABEL,
|
label: DeleteStepAction.LABEL,
|
||||||
run: () => {
|
run: () => {
|
||||||
|
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentJobStep);
|
||||||
self._jobService.deleteJobStep(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
|
self._jobService.deleteJobStep(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
|
||||||
if (!result || !result.success) {
|
if (!result || !result.success) {
|
||||||
let errorMessage = nls.localize("jobaction.failedToDeleteStep", "Could not delete step '{0}'.\nError: {1}",
|
let errorMessage = nls.localize("jobaction.failedToDeleteStep", "Could not delete step '{0}'.\nError: {1}",
|
||||||
@@ -318,7 +329,8 @@ export class DeleteAlertAction extends Action {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@INotificationService private _notificationService: INotificationService,
|
@INotificationService private _notificationService: INotificationService,
|
||||||
@IJobManagementService private _jobService: IJobManagementService
|
@IJobManagementService private _jobService: IJobManagementService,
|
||||||
|
@ITelemetryService private _telemetryService: ITelemetryService
|
||||||
) {
|
) {
|
||||||
super(DeleteAlertAction.ID, DeleteAlertAction.LABEL);
|
super(DeleteAlertAction.ID, DeleteAlertAction.LABEL);
|
||||||
}
|
}
|
||||||
@@ -332,6 +344,7 @@ export class DeleteAlertAction extends Action {
|
|||||||
[{
|
[{
|
||||||
label: DeleteAlertAction.LABEL,
|
label: DeleteAlertAction.LABEL,
|
||||||
run: () => {
|
run: () => {
|
||||||
|
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentAlert);
|
||||||
self._jobService.deleteAlert(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
|
self._jobService.deleteAlert(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
|
||||||
if (!result || !result.success) {
|
if (!result || !result.success) {
|
||||||
let errorMessage = nls.localize("jobaction.failedToDeleteAlert", "Could not delete alert '{0}'.\nError: {1}",
|
let errorMessage = nls.localize("jobaction.failedToDeleteAlert", "Could not delete alert '{0}'.\nError: {1}",
|
||||||
@@ -397,7 +410,8 @@ export class DeleteOperatorAction extends Action {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@INotificationService private _notificationService: INotificationService,
|
@INotificationService private _notificationService: INotificationService,
|
||||||
@IJobManagementService private _jobService: IJobManagementService
|
@IJobManagementService private _jobService: IJobManagementService,
|
||||||
|
@ITelemetryService private _telemetryService: ITelemetryService
|
||||||
) {
|
) {
|
||||||
super(DeleteOperatorAction.ID, DeleteOperatorAction.LABEL);
|
super(DeleteOperatorAction.ID, DeleteOperatorAction.LABEL);
|
||||||
}
|
}
|
||||||
@@ -411,6 +425,7 @@ export class DeleteOperatorAction extends Action {
|
|||||||
[{
|
[{
|
||||||
label: DeleteOperatorAction.LABEL,
|
label: DeleteOperatorAction.LABEL,
|
||||||
run: () => {
|
run: () => {
|
||||||
|
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentOperator);
|
||||||
self._jobService.deleteOperator(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
|
self._jobService.deleteOperator(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
|
||||||
if (!result || !result.success) {
|
if (!result || !result.success) {
|
||||||
let errorMessage = nls.localize("jobaction.failedToDeleteOperator", "Could not delete operator '{0}'.\nError: {1}",
|
let errorMessage = nls.localize("jobaction.failedToDeleteOperator", "Could not delete operator '{0}'.\nError: {1}",
|
||||||
@@ -477,7 +492,8 @@ export class DeleteProxyAction extends Action {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@INotificationService private _notificationService: INotificationService,
|
@INotificationService private _notificationService: INotificationService,
|
||||||
@IJobManagementService private _jobService: IJobManagementService
|
@IJobManagementService private _jobService: IJobManagementService,
|
||||||
|
@ITelemetryService private _telemetryService: ITelemetryService
|
||||||
) {
|
) {
|
||||||
super(DeleteProxyAction.ID, DeleteProxyAction.LABEL);
|
super(DeleteProxyAction.ID, DeleteProxyAction.LABEL);
|
||||||
}
|
}
|
||||||
@@ -491,6 +507,7 @@ export class DeleteProxyAction extends Action {
|
|||||||
[{
|
[{
|
||||||
label: DeleteProxyAction.LABEL,
|
label: DeleteProxyAction.LABEL,
|
||||||
run: () => {
|
run: () => {
|
||||||
|
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentProxy);
|
||||||
self._jobService.deleteProxy(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
|
self._jobService.deleteProxy(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
|
||||||
if (!result || !result.success) {
|
if (!result || !result.success) {
|
||||||
let errorMessage = nls.localize("jobaction.failedToDeleteProxy", "Could not delete proxy '{0}'.\nError: {1}",
|
let errorMessage = nls.localize("jobaction.failedToDeleteProxy", "Could not delete proxy '{0}'.\nError: {1}",
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
<!-- Job History details -->
|
<!-- Job History details -->
|
||||||
<div class='history-details'>
|
<div class='history-details'>
|
||||||
<!-- Previous run list -->
|
<!-- 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">
|
<table *ngIf="_showPreviousRuns === true">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="date-column">
|
<td class="date-column">
|
||||||
@@ -89,7 +89,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<h3 *ngIf="_showPreviousRuns === false" style="text-align: center">No Previous Runs Available</h3>
|
<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>
|
</div>
|
||||||
<!-- Job Steps -->
|
<!-- Job Steps -->
|
||||||
<div class="job-steps" id="job-steps">
|
<div class="job-steps" id="job-steps">
|
||||||
@@ -154,8 +156,8 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<div #jobsteps style="height: 100%">
|
<div #jobsteps style="flex: 1 1 auto; position: relative">
|
||||||
<jobstepsview-component *ngIf="showSteps === true"></jobstepsview-component>
|
<jobstepsview-component *ngIf="showSteps === true" style="position: absolute; height: 100%; width: 100%"></jobstepsview-component>
|
||||||
</div>
|
</div>
|
||||||
<h3 *ngIf="showSteps === false">No Steps Available</h3>
|
<h3 *ngIf="showSteps === false">No Steps Available</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,13 +23,14 @@ import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/work
|
|||||||
import { attachListStyler } from 'vs/platform/theme/common/styler';
|
import { attachListStyler } from 'vs/platform/theme/common/styler';
|
||||||
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
|
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
|
||||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
|
||||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||||
import { JobManagementView } from 'sql/parts/jobManagement/views/jobManagementView';
|
import { JobManagementView } from 'sql/parts/jobManagement/views/jobManagementView';
|
||||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||||
import { IDashboardService } from 'sql/services/dashboard/common/dashboardService';
|
import { IDashboardService } from 'sql/services/dashboard/common/dashboardService';
|
||||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
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';
|
export const DASHBOARD_SELECTOR: string = 'jobhistory-component';
|
||||||
|
|
||||||
@@ -77,7 +78,8 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
|
|||||||
@Inject(IContextMenuService) private contextMenuService: IContextMenuService,
|
@Inject(IContextMenuService) private contextMenuService: IContextMenuService,
|
||||||
@Inject(IJobManagementService) private _jobManagementService: IJobManagementService,
|
@Inject(IJobManagementService) private _jobManagementService: IJobManagementService,
|
||||||
@Inject(IKeybindingService) keybindingService: IKeybindingService,
|
@Inject(IKeybindingService) keybindingService: IKeybindingService,
|
||||||
@Inject(IDashboardService) dashboardService: IDashboardService
|
@Inject(IDashboardService) dashboardService: IDashboardService,
|
||||||
|
@Inject(ITelemetryService) private _telemetryService: ITelemetryService
|
||||||
) {
|
) {
|
||||||
super(commonService, dashboardService, contextMenuService, keybindingService, instantiationService);
|
super(commonService, dashboardService, contextMenuService, keybindingService, instantiationService);
|
||||||
this._treeController = new JobHistoryController();
|
this._treeController = new JobHistoryController();
|
||||||
@@ -141,9 +143,9 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
|
|||||||
renderer: this._treeRenderer
|
renderer: this._treeRenderer
|
||||||
}, {verticalScrollMode: ScrollbarVisibility.Visible});
|
}, {verticalScrollMode: ScrollbarVisibility.Visible});
|
||||||
this._register(attachListStyler(this._tree, this.themeService));
|
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.initActionBar();
|
||||||
|
this._telemetryService.publicLog(TelemetryKeys.JobHistoryView);
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadHistory() {
|
private loadHistory() {
|
||||||
@@ -293,6 +295,7 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
|
|||||||
if (historyDetails && statusBar) {
|
if (historyDetails && statusBar) {
|
||||||
let historyBottom = historyDetails.getBoundingClientRect().bottom;
|
let historyBottom = historyDetails.getBoundingClientRect().bottom;
|
||||||
let statusTop = statusBar.getBoundingClientRect().top;
|
let statusTop = statusBar.getBoundingClientRect().top;
|
||||||
|
|
||||||
let height: number = statusTop - historyBottom - JobHistoryComponent.HEADING_HEIGHT;
|
let height: number = statusTop - historyBottom - JobHistoryComponent.HEADING_HEIGHT;
|
||||||
|
|
||||||
if (this._table) {
|
if (this._table) {
|
||||||
@@ -302,14 +305,7 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this._tree) {
|
if (this._tree) {
|
||||||
this._tree.layout(height);
|
this._tree.layout(dom.getContentHeight(this._tableContainer.nativeElement));
|
||||||
}
|
|
||||||
|
|
||||||
if (this._jobStepsView) {
|
|
||||||
let element = this._jobStepsView.nativeElement as HTMLElement;
|
|
||||||
if (element) {
|
|
||||||
element.style.height = height + 'px';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,17 +177,17 @@ table.step-list tr.step-row td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.history-details {
|
.history-details {
|
||||||
height: 100%;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-details > .job-steps {
|
.history-details > .job-steps {
|
||||||
display: block;
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
border-left: 3px solid #f4f4f4;
|
border-left: 3px solid #f4f4f4;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
height: 100%;
|
flex-direction: column;
|
||||||
width: 90%;
|
width: 100%;
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vs-dark .history-details > .job-steps {
|
.vs-dark .history-details > .job-steps {
|
||||||
@@ -241,13 +241,22 @@ table.step-list tr.step-row td {
|
|||||||
width: 140px;
|
width: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.steps-tree .monaco-tree .monaco-tree-row {
|
.step-table {
|
||||||
white-space: normal;
|
flex: 1 1 auto;
|
||||||
min-height: 40px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
jobhistory-component .jobhistory-heading-container {
|
.prev-run-list-container {
|
||||||
display: -webkit-box;
|
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 {
|
jobhistory-component > .jobhistory-heading-container > .icon.in-progress {
|
||||||
@@ -267,4 +276,4 @@ jobhistory-component > .agent-actionbar-container .monaco-action-bar > ul.action
|
|||||||
|
|
||||||
.hc-black jobhistory-component > .agent-actionbar-container .monaco-action-bar > ul.actions-container {
|
.hc-black jobhistory-component > .agent-actionbar-container .monaco-action-bar > ul.actions-container {
|
||||||
border-top: 3px solid #2b56f2;
|
border-top: 3px solid #2b56f2;
|
||||||
}
|
}
|
||||||
@@ -22,4 +22,6 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</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 '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 { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, Injectable, AfterContentChecked } from '@angular/core';
|
||||||
import { attachListStyler } from 'vs/platform/theme/common/styler';
|
import { attachListStyler } from 'vs/platform/theme/common/styler';
|
||||||
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
|
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 { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||||
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
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';
|
export const JOBSTEPSVIEW_SELECTOR: string = 'jobstepsview-component';
|
||||||
|
|
||||||
@@ -36,7 +38,6 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit,
|
|||||||
private _treeDataSource = new JobStepsViewDataSource();
|
private _treeDataSource = new JobStepsViewDataSource();
|
||||||
private _treeRenderer = new JobStepsViewRenderer();
|
private _treeRenderer = new JobStepsViewRenderer();
|
||||||
private _treeFilter = new JobStepsViewFilter();
|
private _treeFilter = new JobStepsViewFilter();
|
||||||
private _pageSize = 1024;
|
|
||||||
|
|
||||||
@ViewChild('table') private _tableContainer: ElementRef;
|
@ViewChild('table') private _tableContainer: ElementRef;
|
||||||
|
|
||||||
@@ -49,7 +50,8 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit,
|
|||||||
@Inject(IInstantiationService) instantiationService: IInstantiationService,
|
@Inject(IInstantiationService) instantiationService: IInstantiationService,
|
||||||
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
|
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
|
||||||
@Inject(IKeybindingService) keybindingService: IKeybindingService,
|
@Inject(IKeybindingService) keybindingService: IKeybindingService,
|
||||||
@Inject(IDashboardService) dashboardService: IDashboardService
|
@Inject(IDashboardService) dashboardService: IDashboardService,
|
||||||
|
@Inject(ITelemetryService) private _telemetryService: ITelemetryService
|
||||||
) {
|
) {
|
||||||
super(commonService, dashboardService, contextMenuService, keybindingService, instantiationService);
|
super(commonService, dashboardService, contextMenuService, keybindingService, instantiationService);
|
||||||
}
|
}
|
||||||
@@ -57,17 +59,8 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit,
|
|||||||
ngAfterContentChecked() {
|
ngAfterContentChecked() {
|
||||||
if (this._jobHistoryComponent.stepRows.length > 0) {
|
if (this._jobHistoryComponent.stepRows.length > 0) {
|
||||||
this._treeDataSource.data = this._jobHistoryComponent.stepRows;
|
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._tree.setInput(new JobStepsViewModel());
|
||||||
|
this.layout();
|
||||||
$('jobstepsview-component .steps-tree .monaco-tree').attr('tabIndex', '-1');
|
$('jobstepsview-component .steps-tree .monaco-tree').attr('tabIndex', '-1');
|
||||||
$('jobstepsview-component .steps-tree .monaco-tree-row').attr('tabIndex', '0');
|
$('jobstepsview-component .steps-tree .monaco-tree-row').attr('tabIndex', '0');
|
||||||
}
|
}
|
||||||
@@ -79,14 +72,20 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit,
|
|||||||
dataSource: this._treeDataSource,
|
dataSource: this._treeDataSource,
|
||||||
filter: this._treeFilter,
|
filter: this._treeFilter,
|
||||||
renderer: this._treeRenderer
|
renderer: this._treeRenderer
|
||||||
}, {verticalScrollMode: ScrollbarVisibility.Visible});
|
}, {verticalScrollMode: ScrollbarVisibility.Visible, horizontalScrollMode: ScrollbarVisibility.Visible });
|
||||||
|
this.layout();
|
||||||
this._register(attachListStyler(this._tree, this.themeService));
|
this._register(attachListStyler(this._tree, this.themeService));
|
||||||
|
this._telemetryService.publicLog(TelemetryKeys.JobStepsView);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onFirstVisible() {
|
public onFirstVisible() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public layout() {
|
public layout() {
|
||||||
|
if (this._tree) {
|
||||||
|
let treeheight = dom.getContentHeight(this._tableContainer.nativeElement);
|
||||||
|
this._tree.layout(treeheight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,5 +78,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
jobstepsview-component {
|
jobstepsview-component {
|
||||||
padding-top: 10px;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import * as DOM from 'vs/base/browser/dom';
|
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 tree from 'vs/base/parts/tree/browser/tree';
|
||||||
import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults';
|
import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults';
|
||||||
import { Promise, TPromise } from 'vs/base/common/winjs.base';
|
import { Promise, TPromise } from 'vs/base/common/winjs.base';
|
||||||
@@ -86,7 +87,7 @@ export class JobStepsViewRenderer implements tree.IRenderer {
|
|||||||
private _statusIcon: HTMLElement;
|
private _statusIcon: HTMLElement;
|
||||||
|
|
||||||
public getHeight(tree: tree.ITree, element: JobStepsViewRow): number {
|
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 {
|
public getTemplateId(tree: tree.ITree, element: JobStepsViewRow | JobStepsViewModel): string {
|
||||||
@@ -118,6 +119,7 @@ export class JobStepsViewRenderer implements tree.IRenderer {
|
|||||||
let stepMessageCol: HTMLElement = DOM.$('div');
|
let stepMessageCol: HTMLElement = DOM.$('div');
|
||||||
stepMessageCol.className = 'tree-message-col';
|
stepMessageCol.className = 'tree-message-col';
|
||||||
stepMessageCol.innerText = element.message;
|
stepMessageCol.innerText = element.message;
|
||||||
|
$(templateData.label).empty();
|
||||||
templateData.label.appendChild(stepIdCol);
|
templateData.label.appendChild(stepIdCol);
|
||||||
templateData.label.appendChild(stepNameCol);
|
templateData.label.appendChild(stepNameCol);
|
||||||
templateData.label.appendChild(stepMessageCol);
|
templateData.label.appendChild(stepMessageCol);
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import { IDashboardService } from 'sql/services/dashboard/common/dashboardServic
|
|||||||
import { escape } from 'sql/base/common/strings';
|
import { escape } from 'sql/base/common/strings';
|
||||||
import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||||
import { tableBackground, cellBackground, cellBorderColor } from 'sql/common/theme/colors';
|
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 JOBSVIEW_SELECTOR: string = 'jobsview-component';
|
||||||
export const ROW_HEIGHT: number = 45;
|
export const ROW_HEIGHT: number = 45;
|
||||||
@@ -106,7 +108,8 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
|
|||||||
@Inject(IInstantiationService) instantiationService: IInstantiationService,
|
@Inject(IInstantiationService) instantiationService: IInstantiationService,
|
||||||
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
|
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
|
||||||
@Inject(IKeybindingService) keybindingService: IKeybindingService,
|
@Inject(IKeybindingService) keybindingService: IKeybindingService,
|
||||||
@Inject(IDashboardService) _dashboardService: IDashboardService
|
@Inject(IDashboardService) _dashboardService: IDashboardService,
|
||||||
|
@Inject(ITelemetryService) private _telemetryService: ITelemetryService
|
||||||
) {
|
) {
|
||||||
super(commonService, _dashboardService, contextMenuService, keybindingService, instantiationService);
|
super(commonService, _dashboardService, contextMenuService, keybindingService, instantiationService);
|
||||||
this._didTabChange = false;
|
this._didTabChange = false;
|
||||||
@@ -127,6 +130,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
|
|||||||
this._visibilityElement = this._gridEl;
|
this._visibilityElement = this._gridEl;
|
||||||
this._parentComponent = this._agentViewComponent;
|
this._parentComponent = this._agentViewComponent;
|
||||||
this._register(this._themeService.onDidColorThemeChange(e => this.updateTheme(e)));
|
this._register(this._themeService.onDidColorThemeChange(e => this.updateTheme(e)));
|
||||||
|
this._telemetryService.publicLog(TelemetryKeys.JobsView);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
@@ -933,19 +937,19 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
|
|||||||
// add steps
|
// add steps
|
||||||
if (this.jobSteps && this.jobSteps[jobId]) {
|
if (this.jobSteps && this.jobSteps[jobId]) {
|
||||||
let steps = this.jobSteps[jobId];
|
let steps = this.jobSteps[jobId];
|
||||||
job[0].JobSteps = steps;
|
job[0].jobSteps = steps;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add schedules
|
// add schedules
|
||||||
if (this.jobSchedules && this.jobSchedules[jobId]) {
|
if (this.jobSchedules && this.jobSchedules[jobId]) {
|
||||||
let schedules = this.jobSchedules[jobId];
|
let schedules = this.jobSchedules[jobId];
|
||||||
job[0].JobSchedules = schedules;
|
job[0].jobSchedules = schedules;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add alerts
|
// add alerts
|
||||||
if (this.jobAlerts && this.jobAlerts[jobId]) {
|
if (this.jobAlerts && this.jobAlerts[jobId]) {
|
||||||
let alerts = this.jobAlerts[jobId];
|
let alerts = this.jobAlerts[jobId];
|
||||||
job[0].Alerts = alerts;
|
job[0].alerts = alerts;
|
||||||
}
|
}
|
||||||
return job && job.length > 0 ? job[0] : undefined;
|
return job && job.length > 0 ? job[0] : undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,20 @@ export class QueryTextEditor extends BaseTextEditor {
|
|||||||
if (!this._config) {
|
if (!this._config) {
|
||||||
this._config = new Configuration(undefined, editorWidget.getDomNode());
|
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);
|
let editorHeightUsingMinHeight = Math.max(editorHeightUsingLines, this._minHeight);
|
||||||
this.setHeight(editorHeightUsingMinHeight);
|
this.setHeight(editorHeightUsingMinHeight);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div #editor class="editor" style="flex: 1 1 auto; overflow: hidden;">
|
<div #editor class="editor" style="flex: 1 1 auto; overflow: hidden;">
|
||||||
</div>
|
</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: 10px; orientation: portrait">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
import 'vs/css!./code';
|
import 'vs/css!./code';
|
||||||
|
|
||||||
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter } from '@angular/core';
|
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
|
||||||
|
|
||||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||||
import { AngularDisposable } from 'sql/base/common/lifecycle';
|
import { AngularDisposable } from 'sql/base/common/lifecycle';
|
||||||
@@ -41,11 +41,12 @@ export const CODE_SELECTOR: string = 'code-component';
|
|||||||
selector: CODE_SELECTOR,
|
selector: CODE_SELECTOR,
|
||||||
templateUrl: decodeURI(require.toUrl('./code.component.html'))
|
templateUrl: decodeURI(require.toUrl('./code.component.html'))
|
||||||
})
|
})
|
||||||
export class CodeComponent extends AngularDisposable implements OnInit {
|
export class CodeComponent extends AngularDisposable implements OnInit, OnChanges {
|
||||||
@ViewChild('toolbar', { read: ElementRef }) private toolbarElement: ElementRef;
|
@ViewChild('toolbar', { read: ElementRef }) private toolbarElement: ElementRef;
|
||||||
@ViewChild('moreactions', { read: ElementRef }) private moreactionsElement: ElementRef;
|
@ViewChild('moreactions', { read: ElementRef }) private moreActionsElementRef: ElementRef;
|
||||||
@ViewChild('editor', { read: ElementRef }) private codeElement: ElementRef;
|
@ViewChild('editor', { read: ElementRef }) private codeElement: ElementRef;
|
||||||
@Input() cellModel: ICellModel;
|
@Input() cellModel: ICellModel;
|
||||||
|
@Input() hideVerticalToolbar: boolean = false;
|
||||||
|
|
||||||
@Output() public onContentChanged = new EventEmitter<void>();
|
@Output() public onContentChanged = new EventEmitter<void>();
|
||||||
|
|
||||||
@@ -53,6 +54,10 @@ export class CodeComponent extends AngularDisposable implements OnInit {
|
|||||||
this._model = value;
|
this._model = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Input() set activeCellId(value: string) {
|
||||||
|
this._activeCellId = value;
|
||||||
|
}
|
||||||
|
|
||||||
protected _actionBar: Taskbar;
|
protected _actionBar: Taskbar;
|
||||||
protected _moreActions: ActionBar;
|
protected _moreActions: ActionBar;
|
||||||
private readonly _minimumHeight = 30;
|
private readonly _minimumHeight = 30;
|
||||||
@@ -61,6 +66,8 @@ export class CodeComponent extends AngularDisposable implements OnInit {
|
|||||||
private _editorModel: ITextModel;
|
private _editorModel: ITextModel;
|
||||||
private _uri: string;
|
private _uri: string;
|
||||||
private _model: NotebookModel;
|
private _model: NotebookModel;
|
||||||
|
private _actions: Action[] = [];
|
||||||
|
private _activeCellId: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
|
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
|
||||||
@@ -74,17 +81,38 @@ export class CodeComponent extends AngularDisposable implements OnInit {
|
|||||||
@Inject(INotificationService) private notificationService: INotificationService,
|
@Inject(INotificationService) private notificationService: INotificationService,
|
||||||
) {
|
) {
|
||||||
super();
|
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'))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
||||||
this.updateTheme(this.themeService.getColorTheme());
|
this.updateTheme(this.themeService.getColorTheme());
|
||||||
this.initActionBar();
|
if (!this.hideVerticalToolbar) {
|
||||||
|
this.initActionBar();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges() {
|
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
||||||
this.updateLanguageMode();
|
this.updateLanguageMode();
|
||||||
this.updateModel();
|
this.updateModel();
|
||||||
|
for (let propName in changes) {
|
||||||
|
if (propName === 'activeCellId') {
|
||||||
|
let changedProp = changes[propName];
|
||||||
|
if (this.cellModel.id === changedProp.currentValue) {
|
||||||
|
this.toggleMoreActions(true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.toggleMoreActions(false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterContentInit(): void {
|
ngAfterContentInit(): void {
|
||||||
@@ -98,6 +126,10 @@ export class CodeComponent extends AngularDisposable implements OnInit {
|
|||||||
return this._model;
|
return this._model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get activeCellId(): string {
|
||||||
|
return this._activeCellId;
|
||||||
|
}
|
||||||
|
|
||||||
private createEditor(): void {
|
private createEditor(): void {
|
||||||
let instantiationService = this._instantiationService.createChild(new ServiceCollection([IProgressService, new SimpleProgressService()]));
|
let instantiationService = this._instantiationService.createChild(new ServiceCollection([IProgressService, new SimpleProgressService()]));
|
||||||
this._editor = instantiationService.createInstance(QueryTextEditor);
|
this._editor = instantiationService.createInstance(QueryTextEditor);
|
||||||
@@ -139,19 +171,22 @@ export class CodeComponent extends AngularDisposable implements OnInit {
|
|||||||
this._actionBar.setContent([
|
this._actionBar.setContent([
|
||||||
{ action: runCellAction }
|
{ action: runCellAction }
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
let moreActionsElement = <HTMLElement>this.moreactionsElement.nativeElement;
|
private toggleMoreActions(showIcon: boolean) {
|
||||||
this._moreActions = new ActionBar(moreActionsElement, { orientation: ActionsOrientation.VERTICAL });
|
let context = new CellContext(this.model, this.cellModel);
|
||||||
this._moreActions.context = { target: moreActionsElement };
|
let moreActionsElement = <HTMLElement>this.moreActionsElementRef.nativeElement;
|
||||||
|
if (showIcon) {
|
||||||
let actions: Action[] = [];
|
if (moreActionsElement.childNodes.length > 0) {
|
||||||
actions.push(this._instantiationService.createInstance(AddCellAction, 'codeBefore', localize('codeBefore', 'Insert Code before'), CellTypes.Code, false));
|
moreActionsElement.removeChild(moreActionsElement.childNodes[0]);
|
||||||
actions.push(this._instantiationService.createInstance(AddCellAction, 'codeAfter', localize('codeAfter', 'Insert Code after'), CellTypes.Code, true));
|
}
|
||||||
actions.push(this._instantiationService.createInstance(AddCellAction, 'markdownBefore', localize('markdownBefore', 'Insert Markdown before'), CellTypes.Markdown, false));
|
this._moreActions = new ActionBar(moreActionsElement, { orientation: ActionsOrientation.VERTICAL });
|
||||||
actions.push(this._instantiationService.createInstance(AddCellAction, 'markdownAfter', localize('markdownAfter', 'Insert Markdown after'), CellTypes.Markdown, true));
|
this._moreActions.context = { target: moreActionsElement };
|
||||||
actions.push(this._instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', 'Delete')));
|
this._moreActions.push(this._instantiationService.createInstance(ToggleMoreWidgetAction, this._actions, context), { icon: showIcon, label: false });
|
||||||
|
}
|
||||||
this._moreActions.push(this._instantiationService.createInstance(ToggleMoreWidgetAction, actions, context), { icon: true, label: false });
|
else if (moreActionsElement.childNodes.length > 0) {
|
||||||
|
moreActionsElement.removeChild(moreActionsElement.childNodes[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private createUri(): URI {
|
private createUri(): URI {
|
||||||
@@ -180,8 +215,8 @@ export class CodeComponent extends AngularDisposable implements OnInit {
|
|||||||
let toolbarEl = <HTMLElement>this.toolbarElement.nativeElement;
|
let toolbarEl = <HTMLElement>this.toolbarElement.nativeElement;
|
||||||
toolbarEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
toolbarEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||||
|
|
||||||
let moreactionsEl = <HTMLElement>this.moreactionsElement.nativeElement;
|
let moreActionsEl = <HTMLElement>this.moreActionsElementRef.nativeElement;
|
||||||
moreactionsEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
moreActionsEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ code-component {
|
|||||||
|
|
||||||
code-component .toolbar {
|
code-component .toolbar {
|
||||||
border-right-width: 1px;
|
border-right-width: 1px;
|
||||||
border-right-style: solid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
code-component .toolbarIconRun {
|
code-component .toolbarIconRun {
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export class DeleteCellAction extends CellActionBase {
|
|||||||
|
|
||||||
runCellAction(context: CellContext): Promise<void> {
|
runCellAction(context: CellContext): Promise<void> {
|
||||||
try {
|
try {
|
||||||
context.model.deleteCell(context.cell);
|
context.model.deleteCell(context.cell);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let message = getErrorMessage(error);
|
let message = getErrorMessage(error);
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
-->
|
-->
|
||||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
||||||
<div class="notebook-code" style="flex: 0 0 auto;">
|
<div class="notebook-code" style="flex: 0 0 auto;">
|
||||||
<code-component [cellModel]="cellModel" [model]="model"></code-component>
|
<code-component [cellModel]="cellModel" [model]="model" [activeCellId]="activeCellId"></code-component>
|
||||||
</div>
|
</div>
|
||||||
<div #codeCellOutput class="notebook-output" style="flex: 0 0 auto;">
|
<div style="flex: 0 0 auto; width: 100%; height: 100%; display: block">
|
||||||
<output-area-component *ngIf="cellModel.outputs && cellModel.outputs.length > 0" [cellModel]="cellModel">
|
<output-area-component *ngIf="cellModel.outputs && cellModel.outputs.length > 0" [cellModel]="cellModel">
|
||||||
</output-area-component>
|
</output-area-component>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,15 +2,10 @@
|
|||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
import 'vs/css!./codeCell';
|
|
||||||
|
|
||||||
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core';
|
|
||||||
|
|
||||||
|
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, SimpleChange, OnChanges } from '@angular/core';
|
||||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||||
import { CellView } from 'sql/parts/notebook/cellViews/interfaces';
|
import { CellView } from 'sql/parts/notebook/cellViews/interfaces';
|
||||||
|
|
||||||
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
|
||||||
import * as themeColors from 'vs/workbench/common/theme';
|
|
||||||
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||||
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
||||||
|
|
||||||
@@ -21,24 +16,27 @@ export const CODE_SELECTOR: string = 'code-cell-component';
|
|||||||
selector: CODE_SELECTOR,
|
selector: CODE_SELECTOR,
|
||||||
templateUrl: decodeURI(require.toUrl('./codeCell.component.html'))
|
templateUrl: decodeURI(require.toUrl('./codeCell.component.html'))
|
||||||
})
|
})
|
||||||
export class CodeCellComponent extends CellView implements OnInit {
|
|
||||||
|
export class CodeCellComponent extends CellView implements OnInit, OnChanges {
|
||||||
@ViewChild('codeCellOutput', { read: ElementRef }) private outputPreview: ElementRef;
|
@ViewChild('codeCellOutput', { read: ElementRef }) private outputPreview: ElementRef;
|
||||||
private _model: NotebookModel;
|
|
||||||
@Input() cellModel: ICellModel;
|
@Input() cellModel: ICellModel;
|
||||||
@Input() set model(value: NotebookModel) {
|
@Input() set model(value: NotebookModel) {
|
||||||
this._model = value;
|
this._model = value;
|
||||||
}
|
}
|
||||||
|
@Input() set activeCellId(value: string) {
|
||||||
|
this._activeCellId = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _model: NotebookModel;
|
||||||
|
private _activeCellId: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
|
||||||
this.updateTheme(this.themeService.getColorTheme());
|
|
||||||
if (this.cellModel) {
|
if (this.cellModel) {
|
||||||
this.cellModel.onOutputsChanged(() => {
|
this.cellModel.onOutputsChanged(() => {
|
||||||
this._changeRef.detectChanges();
|
this._changeRef.detectChanges();
|
||||||
@@ -46,18 +44,26 @@ export class CodeCellComponent extends CellView implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo: implement layout
|
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
||||||
public layout() {
|
for (let propName in changes) {
|
||||||
|
if (propName === 'activeCellId') {
|
||||||
}
|
let changedProp = changes[propName];
|
||||||
|
this._activeCellId = changedProp.currentValue;
|
||||||
private updateTheme(theme: IColorTheme): void {
|
break;
|
||||||
let outputElement = <HTMLElement>this.outputPreview.nativeElement;
|
}
|
||||||
outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get model(): NotebookModel {
|
get model(): NotebookModel {
|
||||||
return this._model;
|
return this._model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get activeCellId(): string {
|
||||||
|
return this._activeCellId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Todo: implement layout
|
||||||
|
public layout() {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
-->
|
-->
|
||||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
||||||
<div style="flex: 0 0 auto;">
|
<div #outputarea class="notebook-output" style="flex: 0 0 auto;">
|
||||||
<output-component *ngFor="let output of cellModel.outputs" [cellOutput]="output" [trustedMode] = "cellModel.trustedMode" >
|
<output-component *ngFor="let output of cellModel.outputs" [cellOutput]="output" [trustedMode] = "cellModel.trustedMode" >
|
||||||
</output-component>
|
</output-component>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,9 +3,12 @@
|
|||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
import 'vs/css!./code';
|
import 'vs/css!./code';
|
||||||
import { OnInit, Component, Input, Inject, forwardRef, ChangeDetectorRef } from '@angular/core';
|
import 'vs/css!./outputArea';
|
||||||
|
import { OnInit, Component, Input, Inject, ElementRef, ViewChild, forwardRef, ChangeDetectorRef } from '@angular/core';
|
||||||
import { AngularDisposable } from 'sql/base/common/lifecycle';
|
import { AngularDisposable } from 'sql/base/common/lifecycle';
|
||||||
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||||
|
import * as themeColors from 'vs/workbench/common/theme';
|
||||||
|
import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||||
|
|
||||||
export const OUTPUT_AREA_SELECTOR: string = 'output-area-component';
|
export const OUTPUT_AREA_SELECTOR: string = 'output-area-component';
|
||||||
|
|
||||||
@@ -14,20 +17,30 @@ export const OUTPUT_AREA_SELECTOR: string = 'output-area-component';
|
|||||||
templateUrl: decodeURI(require.toUrl('./outputArea.component.html'))
|
templateUrl: decodeURI(require.toUrl('./outputArea.component.html'))
|
||||||
})
|
})
|
||||||
export class OutputAreaComponent extends AngularDisposable implements OnInit {
|
export class OutputAreaComponent extends AngularDisposable implements OnInit {
|
||||||
|
@ViewChild('outputarea', { read: ElementRef }) private outputArea: ElementRef;
|
||||||
@Input() cellModel: ICellModel;
|
@Input() cellModel: ICellModel;
|
||||||
|
|
||||||
private readonly _minimumHeight = 30;
|
private readonly _minimumHeight = 30;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||||
|
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
ngOnInit(): void {
|
|
||||||
|
ngOnInit() {
|
||||||
|
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
||||||
|
this.updateTheme(this.themeService.getColorTheme());
|
||||||
if (this.cellModel) {
|
if (this.cellModel) {
|
||||||
this.cellModel.onOutputsChanged(() => {
|
this.cellModel.onOutputsChanged(() => {
|
||||||
this._changeRef.detectChanges();
|
this._changeRef.detectChanges();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updateTheme(theme: IColorTheme): void {
|
||||||
|
let outputElement = <HTMLElement>this.outputArea.nativeElement;
|
||||||
|
outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
output-area-component {
|
||||||
code-cell-component {
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
code-cell-component .notebook-output {
|
output-area-component .notebook-output {
|
||||||
border-top-width: 1px;
|
border-top-width: 1px;
|
||||||
border-top-style: solid;
|
border-top-style: solid;
|
||||||
user-select: initial;
|
user-select: initial;
|
||||||
|
padding: 5px 20px 0px;
|
||||||
|
box-shadow: rgba(120, 120, 120, 0.75) 0px -2px 1px -2px;
|
||||||
}
|
}
|
||||||
@@ -6,8 +6,9 @@
|
|||||||
-->
|
-->
|
||||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
||||||
<div class="notebook-text" style="flex: 0 0 auto;">
|
<div class="notebook-text" style="flex: 0 0 auto;">
|
||||||
<code-component *ngIf="isEditMode" [cellModel]="cellModel" (onContentChanged)="handleContentChanged()"></code-component>
|
<code-component *ngIf="isEditMode" [cellModel]="cellModel" (onContentChanged)="handleContentChanged()" [model]="model" [activeCellId]="activeCellId" [hideVerticalToolbar]=true>
|
||||||
|
</code-component>
|
||||||
</div>
|
</div>
|
||||||
<div #preview class="notebook-preview" style="flex: 0 0 auto;" (dblclick)="toggleEditMode()">
|
<div #preview style="flex: 0 0 auto;" (dblclick)="toggleEditMode()">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
import 'vs/css!./textCell';
|
import 'vs/css!./textCell';
|
||||||
|
|
||||||
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core';
|
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, OnChanges, SimpleChange } from '@angular/core';
|
||||||
|
|
||||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||||
import { CellView } from 'sql/parts/notebook/cellViews/interfaces';
|
import { CellView } from 'sql/parts/notebook/cellViews/interfaces';
|
||||||
@@ -15,6 +15,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands';
|
|||||||
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||||
import { ISanitizer, defaultSanitizer } from 'sql/parts/notebook/outputs/sanitizer';
|
import { ISanitizer, defaultSanitizer } from 'sql/parts/notebook/outputs/sanitizer';
|
||||||
import { localize } from 'vs/nls';
|
import { localize } from 'vs/nls';
|
||||||
|
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
||||||
|
|
||||||
export const TEXT_SELECTOR: string = 'text-cell-component';
|
export const TEXT_SELECTOR: string = 'text-cell-component';
|
||||||
|
|
||||||
@@ -22,12 +23,24 @@ export const TEXT_SELECTOR: string = 'text-cell-component';
|
|||||||
selector: TEXT_SELECTOR,
|
selector: TEXT_SELECTOR,
|
||||||
templateUrl: decodeURI(require.toUrl('./textCell.component.html'))
|
templateUrl: decodeURI(require.toUrl('./textCell.component.html'))
|
||||||
})
|
})
|
||||||
export class TextCellComponent extends CellView implements OnInit {
|
export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||||
@ViewChild('preview', { read: ElementRef }) private output: ElementRef;
|
@ViewChild('preview', { read: ElementRef }) private output: ElementRef;
|
||||||
@Input() cellModel: ICellModel;
|
@Input() cellModel: ICellModel;
|
||||||
|
|
||||||
|
@Input() set model(value: NotebookModel) {
|
||||||
|
this._model = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input() set activeCellId(value: string) {
|
||||||
|
this._activeCellId = value;
|
||||||
|
}
|
||||||
|
|
||||||
private _content: string;
|
private _content: string;
|
||||||
private isEditMode: boolean;
|
private isEditMode: boolean;
|
||||||
private _sanitizer: ISanitizer;
|
private _sanitizer: ISanitizer;
|
||||||
|
private _previewCssApplied: boolean = false;
|
||||||
|
private _model: NotebookModel;
|
||||||
|
private _activeCellId: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
|
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
|
||||||
@@ -39,10 +52,6 @@ export class TextCellComponent extends CellView implements OnInit {
|
|||||||
this.isEditMode = false;
|
this.isEditMode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges() {
|
|
||||||
this.updatePreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
//Gets sanitizer from ISanitizer interface
|
//Gets sanitizer from ISanitizer interface
|
||||||
private get sanitizer(): ISanitizer {
|
private get sanitizer(): ISanitizer {
|
||||||
if (this._sanitizer) {
|
if (this._sanitizer) {
|
||||||
@@ -51,13 +60,41 @@ export class TextCellComponent extends CellView implements OnInit {
|
|||||||
return this._sanitizer = defaultSanitizer;
|
return this._sanitizer = defaultSanitizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get model(): NotebookModel {
|
||||||
|
return this._model;
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeCellId(): string {
|
||||||
|
return this._activeCellId;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.updatePreview();
|
||||||
|
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
||||||
|
this.updateTheme(this.themeService.getColorTheme());
|
||||||
|
this.cellModel.onOutputsChanged(e => {
|
||||||
|
this.updatePreview();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
|
||||||
|
for (let propName in changes) {
|
||||||
|
if (propName === 'activeCellId') {
|
||||||
|
let changedProp = changes[propName];
|
||||||
|
this._activeCellId = changedProp.currentValue;
|
||||||
|
this.toggleEditMode(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the preview of markdown component with latest changes
|
* Updates the preview of markdown component with latest changes
|
||||||
* If content is empty and in non-edit mode, default it to 'Double-click to edit'
|
* If content is empty and in non-edit mode, default it to 'Double-click to edit'
|
||||||
* Sanitizes the data to be shown in markdown cell
|
* Sanitizes the data to be shown in markdown cell
|
||||||
*/
|
*/
|
||||||
private updatePreview() {
|
private updatePreview() {
|
||||||
if (this._content !== this.cellModel.source) {
|
if (this._content !== this.cellModel.source || this.cellModel.source.length === 0) {
|
||||||
if (!this.cellModel.source && !this.isEditMode) {
|
if (!this.cellModel.source && !this.isEditMode) {
|
||||||
(<HTMLElement>this.output.nativeElement).innerHTML = localize('doubleClickEdit', 'Double-click to edit');
|
(<HTMLElement>this.output.nativeElement).innerHTML = localize('doubleClickEdit', 'Double-click to edit');
|
||||||
} else {
|
} else {
|
||||||
@@ -79,14 +116,6 @@ export class TextCellComponent extends CellView implements OnInit {
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.updatePreview();
|
|
||||||
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
|
||||||
this.updateTheme(this.themeService.getColorTheme());
|
|
||||||
this.cellModel.onOutputsChanged(e => {
|
|
||||||
this.updatePreview();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Todo: implement layout
|
// Todo: implement layout
|
||||||
public layout() {
|
public layout() {
|
||||||
@@ -98,12 +127,29 @@ export class TextCellComponent extends CellView implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public handleContentChanged(): void {
|
public handleContentChanged(): void {
|
||||||
|
if (!this._previewCssApplied) {
|
||||||
|
this.updatePreviewCssClass();
|
||||||
|
}
|
||||||
this.updatePreview();
|
this.updatePreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
public toggleEditMode(): void {
|
public toggleEditMode(editMode?: boolean): void {
|
||||||
this.isEditMode = !this.isEditMode;
|
this.isEditMode = editMode !== undefined? editMode : !this.isEditMode;
|
||||||
|
this.updatePreviewCssClass();
|
||||||
this.updatePreview();
|
this.updatePreview();
|
||||||
this._changeRef.detectChanges();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/sql/parts/notebook/media/dark/save_inverse.svg
Normal file
1
src/sql/parts/notebook/media/dark/save_inverse.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>save_inverse</title><path class="cls-1" d="M14,1a1,1,0,0,1,.39.08,1,1,0,0,1,.53.53A1,1,0,0,1,15,2V15H2.79L1,13.2V2a1,1,0,0,1,.08-.39,1,1,0,0,1,.53-.53A1,1,0,0,1,2,1Zm0,1H13V8H3V2H2V12.79L3.2,14H4V10h7v4h3ZM4,7h8V2H4Zm6,4H5v3H6V12H7v2h3Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
1
src/sql/parts/notebook/media/light/save.svg
Normal file
1
src/sql/parts/notebook/media/light/save.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>save</title><path d="M14,1a1,1,0,0,1,.39.08,1,1,0,0,1,.53.53A1,1,0,0,1,15,2V15H2.79L1,13.2V2a1,1,0,0,1,.08-.39,1,1,0,0,1,.53-.53A1,1,0,0,1,2,1Zm0,1H13V8H3V2H2V12.79L3.2,14H4V10h7v4h3ZM4,7h8V2H4Zm6,4H5v3H6V12H7v2h3Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 323 B |
@@ -14,6 +14,7 @@ import { ICellModelOptions, IModelFactory, FutureInternal } from './modelInterfa
|
|||||||
import * as notebookUtils from '../notebookUtils';
|
import * as notebookUtils from '../notebookUtils';
|
||||||
import { CellTypes, CellType, NotebookChangeType } from 'sql/parts/notebook/models/contracts';
|
import { CellTypes, CellType, NotebookChangeType } from 'sql/parts/notebook/models/contracts';
|
||||||
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||||
|
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
||||||
|
|
||||||
let modelId = 0;
|
let modelId = 0;
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ export class CellModel implements ICellModel {
|
|||||||
private _active: boolean;
|
private _active: boolean;
|
||||||
private _cellUri: URI;
|
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++}`;
|
this.id = `${modelId++}`;
|
||||||
CellModel.CreateLanguageMappings();
|
CellModel.CreateLanguageMappings();
|
||||||
// Do nothing for now
|
// Do nothing for now
|
||||||
@@ -222,18 +223,48 @@ export class CellModel implements ICellModel {
|
|||||||
if (output) {
|
if (output) {
|
||||||
// deletes transient node in the serialized JSON
|
// deletes transient node in the serialized JSON
|
||||||
delete output['transient'];
|
delete output['transient'];
|
||||||
this._outputs.push(output);
|
this._outputs.push(this.rewriteOutputUrls(output));
|
||||||
this.fireOutputsChanged();
|
this.fireOutputsChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private rewriteOutputUrls(output: nb.ICellOutput): nb.ICellOutput {
|
||||||
|
// Only rewrite if this is coming back during execution, not when loading from disk.
|
||||||
|
// A good approximation is that the model has a future (needed for execution)
|
||||||
|
if (this.future) {
|
||||||
|
try {
|
||||||
|
let result = output as nb.IDisplayResult;
|
||||||
|
if (result && result.data && result.data['text/html']) {
|
||||||
|
let nbm = (this as CellModel).options.notebook as NotebookModel;
|
||||||
|
if (nbm.hadoopConnection) {
|
||||||
|
let host = nbm.hadoopConnection.host;
|
||||||
|
let html = result.data['text/html'];
|
||||||
|
html = html.replace(/(https?:\/\/mssql-master.*\/proxy)(.*)/g, function (a, b, c) {
|
||||||
|
let ret = '';
|
||||||
|
if (b !== '') {
|
||||||
|
ret = 'https://' + host + ':30443/gateway/default/yarn/proxy';
|
||||||
|
}
|
||||||
|
if (c !== '') {
|
||||||
|
ret = ret + c;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
(<nb.IDisplayResult>output).data['text/html'] = html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
private getDisplayId(msg: nb.IIOPubMessage): string | undefined {
|
private getDisplayId(msg: nb.IIOPubMessage): string | undefined {
|
||||||
let transient = (msg.content.transient || {});
|
let transient = (msg.content.transient || {});
|
||||||
return transient['display_id'] as string;
|
return transient['display_id'] as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
public toJSON(): nb.ICell {
|
public toJSON(): nb.ICellContents {
|
||||||
let cellJson: Partial<nb.ICell> = {
|
let cellJson: Partial<nb.ICellContents> = {
|
||||||
cell_type: this._cellType,
|
cell_type: this._cellType,
|
||||||
source: this._source,
|
source: this._source,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -244,10 +275,10 @@ export class CellModel implements ICellModel {
|
|||||||
cellJson.outputs = this._outputs;
|
cellJson.outputs = this._outputs;
|
||||||
cellJson.execution_count = 1; // TODO: keep track of actual execution count
|
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) {
|
if (!cell) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { ClientSession } from './clientSession';
|
|||||||
|
|
||||||
export class ModelFactory implements IModelFactory {
|
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);
|
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 { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
||||||
import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection';
|
import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection';
|
||||||
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
|
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement';
|
||||||
|
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||||
|
|
||||||
export interface IClientSessionOptions {
|
export interface IClientSessionOptions {
|
||||||
notebookUri: URI;
|
notebookUri: URI;
|
||||||
@@ -328,6 +329,14 @@ export interface INotebookModel {
|
|||||||
* Notifies the notebook of a change in the cell
|
* Notifies the notebook of a change in the cell
|
||||||
*/
|
*/
|
||||||
onCellChange(cell: ICellModel, change: NotebookChangeType): void;
|
onCellChange(cell: ICellModel, change: NotebookChangeType): void;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push edit operations, basically editing the model. This is the preferred way of
|
||||||
|
* editing the model. Long-term, this will ensure edit operations can be added to the undo stack
|
||||||
|
* @param edits The edit operations to perform
|
||||||
|
*/
|
||||||
|
pushEditOperations(edits: ISingleNotebookEditOperation[]): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICellModelOptions {
|
export interface ICellModelOptions {
|
||||||
@@ -348,7 +357,7 @@ export interface ICellModel {
|
|||||||
readonly onOutputsChanged: Event<ReadonlyArray<nb.ICellOutput>>;
|
readonly onOutputsChanged: Event<ReadonlyArray<nb.ICellOutput>>;
|
||||||
setFuture(future: FutureInternal): void;
|
setFuture(future: FutureInternal): void;
|
||||||
equals(cellModel: ICellModel): boolean;
|
equals(cellModel: ICellModel): boolean;
|
||||||
toJSON(): nb.ICell;
|
toJSON(): nb.ICellContents;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FutureInternal extends nb.IFuture {
|
export interface FutureInternal extends nb.IFuture {
|
||||||
@@ -357,7 +366,7 @@ export interface FutureInternal extends nb.IFuture {
|
|||||||
|
|
||||||
export interface IModelFactory {
|
export interface IModelFactory {
|
||||||
|
|
||||||
createCell(cell: nb.ICell, options: ICellModelOptions): ICellModel;
|
createCell(cell: nb.ICellContents, options: ICellModelOptions): ICellModel;
|
||||||
createClientSession(options: IClientSessionOptions): IClientSession;
|
createClientSession(options: IClientSessionOptions): IClientSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
|||||||
import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection';
|
import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection';
|
||||||
import { INotification, Severity } from 'vs/platform/notification/common/notification';
|
import { INotification, Severity } from 'vs/platform/notification/common/notification';
|
||||||
import { Schemas } from 'vs/base/common/network';
|
import { Schemas } from 'vs/base/common/network';
|
||||||
|
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Used to control whether a message in a dialog/wizard is displayed as an error,
|
* Used to control whether a message in a dialog/wizard is displayed as an error,
|
||||||
@@ -237,7 +238,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createCell(cellType: CellType): ICellModel {
|
private createCell(cellType: CellType): ICellModel {
|
||||||
let singleCell: nb.ICell = {
|
let singleCell: nb.ICellContents = {
|
||||||
cell_type: cellType,
|
cell_type: cellType,
|
||||||
source: '',
|
source: '',
|
||||||
metadata: {},
|
metadata: {},
|
||||||
@@ -263,6 +264,25 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pushEditOperations(edits: ISingleNotebookEditOperation[]): void {
|
||||||
|
if (this.inErrorState || !this._cells) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let edit of edits) {
|
||||||
|
let newCells: ICellModel[] = [];
|
||||||
|
if (edit.cell) {
|
||||||
|
// TODO: should we validate and complete required missing parameters?
|
||||||
|
let contents: nb.ICellContents = edit.cell as nb.ICellContents;
|
||||||
|
newCells.push(this.notebookOptions.factory.createCell(contents, { notebook: this, isTrusted: this._trustedMode }));
|
||||||
|
}
|
||||||
|
this._cells.splice(edit.range.start, edit.range.end - edit.range.start, ...newCells);
|
||||||
|
this._contentChangedEmitter.fire({
|
||||||
|
changeType: NotebookChangeType.CellsAdded
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public get activeCell(): ICellModel {
|
public get activeCell(): ICellModel {
|
||||||
return this._activeCell;
|
return this._activeCell;
|
||||||
}
|
}
|
||||||
@@ -281,9 +301,14 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
|||||||
notebookManager: this.notebookManager,
|
notebookManager: this.notebookManager,
|
||||||
notificationService: this.notebookOptions.notificationService
|
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._clientSession.initialize(this._hadoopConnection);
|
||||||
this._sessionLoadFinished = this._clientSession.ready.then(async () => {
|
this._sessionLoadFinished = this._clientSession.ready.then(async () => {
|
||||||
if (this._clientSession.isInErrorState) {
|
if (this._clientSession.isInErrorState) {
|
||||||
@@ -389,7 +414,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
|||||||
|
|
||||||
// Get default language if saved in notebook file
|
// Get default language if saved in notebook file
|
||||||
// Otherwise, default to python
|
// Otherwise, default to python
|
||||||
private getDefaultLanguageInfo(notebook: nb.INotebook): nb.ILanguageInfo {
|
private getDefaultLanguageInfo(notebook: nb.INotebookContents): nb.ILanguageInfo {
|
||||||
return notebook!.metadata!.language_info || {
|
return notebook!.metadata!.language_info || {
|
||||||
name: 'python',
|
name: 'python',
|
||||||
version: '',
|
version: '',
|
||||||
@@ -398,7 +423,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get default kernel info if saved in notebook file
|
// 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;
|
return notebook!.metadata!.kernelspec;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,8 +515,8 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
|||||||
/**
|
/**
|
||||||
* Serialize the model to JSON.
|
* Serialize the model to JSON.
|
||||||
*/
|
*/
|
||||||
toJSON(): nb.INotebook {
|
toJSON(): nb.INotebookContents {
|
||||||
let cells: nb.ICell[] = this.cells.map(c => c.toJSON());
|
let cells: nb.ICellContents[] = this.cells.map(c => c.toJSON());
|
||||||
let metadata = Object.create(null) as nb.INotebookMetadata;
|
let metadata = Object.create(null) as nb.INotebookMetadata;
|
||||||
// TODO update language and kernel when these change
|
// TODO update language and kernel when these change
|
||||||
metadata.kernelspec = this._savedKernelInfo;
|
metadata.kernelspec = this._savedKernelInfo;
|
||||||
|
|||||||
@@ -140,9 +140,16 @@ export class SparkMagicContexts {
|
|||||||
* @param savedKernelInfo kernel info loaded from
|
* @param savedKernelInfo kernel info loaded from
|
||||||
*/
|
*/
|
||||||
public static getDefaultKernel(specs: nb.IAllKernels, connectionInfo: IConnectionProfile, savedKernelInfo: nb.IKernelInfo, notificationService: INotificationService): nb.IKernelSpec {
|
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;
|
let profile = connectionInfo as IConnectionProfile;
|
||||||
if (specs && connectionInfo && profile.providerName === notebookConstants.hadoopKnoxProviderName) {
|
if (foundSavedKernelInSpecs && specs && connectionInfo && profile.providerName === notebookConstants.hadoopKnoxProviderName) {
|
||||||
// set default kernel to default spark kernel if profile exists
|
// set default kernel to default spark kernel if profile exists
|
||||||
// otherwise, set default to kernel info loaded from existing file
|
// otherwise, set default to kernel info loaded from existing file
|
||||||
defaultKernel = !savedKernelInfo ? specs.kernels.find((spec) => spec.name === notebookConstants.defaultSparkKernel) : savedKernelInfo;
|
defaultKernel = !savedKernelInfo ? specs.kernels.find((spec) => spec.name === notebookConstants.defaultSparkKernel) : savedKernelInfo;
|
||||||
|
|||||||
@@ -7,12 +7,12 @@
|
|||||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
<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; height: 36px">
|
||||||
</div>
|
</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>
|
<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">
|
<code-cell-component *ngIf="cell.cellType === 'code'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId">
|
||||||
</code-cell-component>
|
</code-cell-component>
|
||||||
<text-cell-component *ngIf="cell.cellType === 'markdown'" [cellModel]="cell">
|
<text-cell-component *ngIf="cell.cellType === 'markdown'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId">
|
||||||
</text-cell-component>
|
</text-cell-component>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ import './notebookStyles';
|
|||||||
|
|
||||||
import { nb } from 'sqlops';
|
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 URI from 'vs/base/common/uri';
|
|
||||||
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||||
import * as themeColors from 'vs/workbench/common/theme';
|
import * as themeColors from 'vs/workbench/common/theme';
|
||||||
import { INotificationService, INotification } from 'vs/platform/notification/common/notification';
|
import { INotificationService, INotification } from 'vs/platform/notification/common/notification';
|
||||||
@@ -18,12 +17,12 @@ import { localize } from 'vs/nls';
|
|||||||
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
|
||||||
import { AngularDisposable } from 'sql/base/common/lifecycle';
|
import { AngularDisposable } from 'sql/base/common/lifecycle';
|
||||||
|
|
||||||
import { CellTypes, CellType, NotebookChangeType } from 'sql/parts/notebook/models/contracts';
|
import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts';
|
||||||
import { ICellModel, IModelFactory } from 'sql/parts/notebook/models/modelInterfaces';
|
import { ICellModel, IModelFactory, notebookConstants } from 'sql/parts/notebook/models/modelInterfaces';
|
||||||
import { IConnectionManagementService, IConnectionDialogService } from 'sql/parts/connection/common/connectionManagement';
|
import { IConnectionManagementService, IConnectionDialogService } from 'sql/parts/connection/common/connectionManagement';
|
||||||
import { INotebookService, INotebookParams, INotebookManager } from 'sql/services/notebook/notebookService';
|
import { INotebookService, INotebookParams, INotebookManager, INotebookEditor } from 'sql/services/notebook/notebookService';
|
||||||
import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService';
|
import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService';
|
||||||
import { NotebookModel, ErrorInfo, MessageLevel, NotebookContentChange } from 'sql/parts/notebook/models/notebookModel';
|
import { NotebookModel, NotebookContentChange } from 'sql/parts/notebook/models/notebookModel';
|
||||||
import { ModelFactory } from 'sql/parts/notebook/models/modelFactory';
|
import { ModelFactory } from 'sql/parts/notebook/models/modelFactory';
|
||||||
import * as notebookUtils from './notebookUtils';
|
import * as notebookUtils from './notebookUtils';
|
||||||
import { Deferred } from 'sql/base/common/promise';
|
import { Deferred } from 'sql/base/common/promise';
|
||||||
@@ -31,8 +30,17 @@ import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
|
|||||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||||
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||||
import { KernelsDropdown, AttachToDropdown, AddCellAction, TrustedAction } from 'sql/parts/notebook/notebookActions';
|
import { KernelsDropdown, AttachToDropdown, AddCellAction, TrustedAction, SaveNotebookAction } from 'sql/parts/notebook/notebookActions';
|
||||||
import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
|
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 { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
|
||||||
|
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
|
||||||
|
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||||
|
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||||
|
|
||||||
export const NOTEBOOK_SELECTOR: string = 'notebook-component';
|
export const NOTEBOOK_SELECTOR: string = 'notebook-component';
|
||||||
|
|
||||||
@@ -41,7 +49,7 @@ export const NOTEBOOK_SELECTOR: string = 'notebook-component';
|
|||||||
selector: NOTEBOOK_SELECTOR,
|
selector: NOTEBOOK_SELECTOR,
|
||||||
templateUrl: decodeURI(require.toUrl('./notebook.component.html'))
|
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;
|
@ViewChild('toolbar', { read: ElementRef }) private toolbar: ElementRef;
|
||||||
private _model: NotebookModel;
|
private _model: NotebookModel;
|
||||||
private _isInErrorState: boolean = false;
|
private _isInErrorState: boolean = false;
|
||||||
@@ -54,6 +62,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
|
|||||||
private _modelRegisteredDeferred = new Deferred<NotebookModel>();
|
private _modelRegisteredDeferred = new Deferred<NotebookModel>();
|
||||||
private profile: IConnectionProfile;
|
private profile: IConnectionProfile;
|
||||||
private _trustedAction: TrustedAction;
|
private _trustedAction: TrustedAction;
|
||||||
|
private _activeCellId: string;
|
||||||
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -61,30 +70,64 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
|
|||||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||||
@Inject(IConnectionManagementService) private connectionManagementService: IConnectionManagementService,
|
@Inject(IConnectionManagementService) private connectionManagementService: IConnectionManagementService,
|
||||||
|
@Inject(IObjectExplorerService) private objectExplorerService: IObjectExplorerService,
|
||||||
|
@Inject(IEditorService) private editorService: IEditorService,
|
||||||
@Inject(INotificationService) private notificationService: INotificationService,
|
@Inject(INotificationService) private notificationService: INotificationService,
|
||||||
@Inject(INotebookService) private notebookService: INotebookService,
|
@Inject(INotebookService) private notebookService: INotebookService,
|
||||||
@Inject(IBootstrapParams) private notebookParams: INotebookParams,
|
@Inject(IBootstrapParams) private _notebookParams: INotebookParams,
|
||||||
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
|
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
|
||||||
@Inject(IContextMenuService) private contextMenuService: IContextMenuService,
|
@Inject(IContextMenuService) private contextMenuService: IContextMenuService,
|
||||||
@Inject(IContextViewService) private contextViewService: IContextViewService,
|
@Inject(IContextViewService) private contextViewService: IContextViewService,
|
||||||
@Inject(IConnectionDialogService) private connectionDialogService: IConnectionDialogService
|
@Inject(IConnectionDialogService) private connectionDialogService: IConnectionDialogService,
|
||||||
|
@Inject(IContextKeyService) private contextKeyService: IContextKeyService,
|
||||||
|
@Inject(IMenuService) private menuService: IMenuService,
|
||||||
|
@Inject(IKeybindingService) private keybindingService: IKeybindingService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.profile = this.notebookParams!.profile;
|
this.updateProfile();
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updateProfile(): void {
|
||||||
|
this.profile = this.notebookParams!.profile;
|
||||||
|
if (!this.profile) {
|
||||||
|
// use global connection if possible
|
||||||
|
let profile = TaskUtilities.getCurrentGlobalConnection(this.objectExplorerService, this.connectionManagementService, this.editorService);
|
||||||
|
// TODO use generic method to match kernel with valid connection that's compatible. For now, we only have 1
|
||||||
|
if (profile && profile.providerName === notebookConstants.hadoopKnoxProviderName) {
|
||||||
|
this.profile = profile;
|
||||||
|
} else {
|
||||||
|
// if not, try 1st active connection that matches our filter
|
||||||
|
let profiles = this.connectionManagementService.getActiveConnections([notebookConstants.hadoopKnoxProviderName]);
|
||||||
|
if (profiles && profiles.length > 0) {
|
||||||
|
this.profile = profiles[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
|
||||||
this.updateTheme(this.themeService.getColorTheme());
|
this.updateTheme(this.themeService.getColorTheme());
|
||||||
|
this.notebookService.addNotebookEditor(this);
|
||||||
this.initActionBar();
|
this.initActionBar();
|
||||||
this.doLoad();
|
this.doLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.notebookService) {
|
||||||
|
this.notebookService.removeNotebookEditor(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public get model(): NotebookModel {
|
public get model(): NotebookModel {
|
||||||
return this._model;
|
return this._model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get activeCellId(): string {
|
||||||
|
return this._activeCellId;
|
||||||
|
}
|
||||||
|
|
||||||
public get modelRegistered(): Promise<NotebookModel> {
|
public get modelRegistered(): Promise<NotebookModel> {
|
||||||
return this._modelRegisteredDeferred.promise;
|
return this._modelRegisteredDeferred.promise;
|
||||||
}
|
}
|
||||||
@@ -98,7 +141,10 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
|
|||||||
toolbarEl.style.borderBottomColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
|
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 (cell !== this._activeCell) {
|
||||||
if (this._activeCell) {
|
if (this._activeCell) {
|
||||||
this._activeCell.active = false;
|
this._activeCell.active = false;
|
||||||
@@ -106,10 +152,21 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
|
|||||||
this._activeCell = cell;
|
this._activeCell = cell;
|
||||||
this._activeCell.active = true;
|
this._activeCell.active = true;
|
||||||
this._model.activeCell = this._activeCell;
|
this._model.activeCell = this._activeCell;
|
||||||
|
this._activeCellId = cell.id;
|
||||||
this._changeRef.detectChanges();
|
this._changeRef.detectChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Add cell based on cell type
|
||||||
public addCell(cellType: CellType)
|
public addCell(cellType: CellType)
|
||||||
{
|
{
|
||||||
@@ -165,16 +222,16 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async loadModel(): Promise<void> {
|
private async loadModel(): Promise<void> {
|
||||||
this.notebookManager = await this.notebookService.getOrCreateNotebookManager(this.notebookParams.providerId, this.notebookParams.notebookUri);
|
this.notebookManager = await this.notebookService.getOrCreateNotebookManager(this._notebookParams.providerId, this._notebookParams.notebookUri);
|
||||||
let model = new NotebookModel({
|
let model = new NotebookModel({
|
||||||
factory: this.modelFactory,
|
factory: this.modelFactory,
|
||||||
notebookUri: this.notebookParams.notebookUri,
|
notebookUri: this._notebookParams.notebookUri,
|
||||||
connectionService: this.connectionManagementService,
|
connectionService: this.connectionManagementService,
|
||||||
notificationService: this.notificationService,
|
notificationService: this.notificationService,
|
||||||
notebookManager: this.notebookManager
|
notebookManager: this.notebookManager
|
||||||
}, false, this.profile);
|
}, false, this.profile);
|
||||||
model.onError((errInfo: INotification) => this.handleModelError(errInfo));
|
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));
|
model.contentChanged((change) => this.handleContentChanged(change));
|
||||||
this._model = model;
|
this._model = model;
|
||||||
this.updateToolbarComponents(this._model.trustedMode);
|
this.updateToolbarComponents(this._model.trustedMode);
|
||||||
@@ -195,10 +252,10 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private get modelFactory(): IModelFactory {
|
private get modelFactory(): IModelFactory {
|
||||||
if (!this.notebookParams.modelFactory) {
|
if (!this._notebookParams.modelFactory) {
|
||||||
this.notebookParams.modelFactory = new ModelFactory();
|
this._notebookParams.modelFactory = new ModelFactory();
|
||||||
}
|
}
|
||||||
return this.notebookParams.modelFactory;
|
return this._notebookParams.modelFactory;
|
||||||
}
|
}
|
||||||
private handleModelError(notification: INotification): void {
|
private handleModelError(notification: INotification): void {
|
||||||
this.notificationService.notify(notification);
|
this.notificationService.notify(notification);
|
||||||
@@ -233,9 +290,6 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
|
|||||||
attachTodropdwon.render(attachToContainer);
|
attachTodropdwon.render(attachToContainer);
|
||||||
attachSelectBoxStyler(attachTodropdwon, this.themeService);
|
attachSelectBoxStyler(attachTodropdwon, this.themeService);
|
||||||
|
|
||||||
let attachToInfoText = document.createElement('div');
|
|
||||||
attachToInfoText.className = 'notebook-info-label';
|
|
||||||
attachToInfoText.innerText = 'Attach To: ';
|
|
||||||
|
|
||||||
let addCodeCellButton = new AddCellAction('notebook.AddCodeCell', localize('code', 'Code'), 'notebook-button icon-add');
|
let addCodeCellButton = new AddCellAction('notebook.AddCodeCell', localize('code', 'Code'), 'notebook-button icon-add');
|
||||||
addCodeCellButton.cellType = CellTypes.Code;
|
addCodeCellButton.cellType = CellTypes.Code;
|
||||||
@@ -246,21 +300,44 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
|
|||||||
this._trustedAction = this.instantiationService.createInstance(TrustedAction, 'notebook.Trusted');
|
this._trustedAction = this.instantiationService.createInstance(TrustedAction, 'notebook.Trusted');
|
||||||
this._trustedAction.enabled = false;
|
this._trustedAction.enabled = false;
|
||||||
|
|
||||||
|
let saveNotebookButton = this.instantiationService.createInstance(SaveNotebookAction, 'notebook.SaveNotebook', localize('save', 'Save'), 'notebook-button icon-save');
|
||||||
|
|
||||||
|
// Get all of the menu contributions that use the ID 'notebook/toolbar'.
|
||||||
|
// Then, find all groups (currently we don't leverage the contributed
|
||||||
|
// groups functionality for the notebook toolbar), and fill in the 'primary'
|
||||||
|
// array with items that don't list a group. Finally, add any actions from
|
||||||
|
// the primary array to the end of the toolbar.
|
||||||
|
const notebookBarMenu = this.menuService.createMenu(MenuId.NotebookToolbar, this.contextKeyService);
|
||||||
|
let groups = notebookBarMenu.getActions({ arg: null, shouldForwardArgs: true });
|
||||||
|
let primary: IAction[] = [];
|
||||||
|
let secondary: IAction[] = [];
|
||||||
|
fillInActions(groups, {primary, secondary}, false, (group: string) => group === undefined);
|
||||||
|
|
||||||
let taskbar = <HTMLElement>this.toolbar.nativeElement;
|
let taskbar = <HTMLElement>this.toolbar.nativeElement;
|
||||||
this._actionBar = new Taskbar(taskbar, this.contextMenuService);
|
this._actionBar = new Taskbar(taskbar, this.contextMenuService, { actionItemProvider: action => this.actionItemProvider(action as Action)});
|
||||||
this._actionBar.context = this;
|
this._actionBar.context = this;
|
||||||
this._actionBar.setContent([
|
this._actionBar.setContent([
|
||||||
{ element: kernelContainer },
|
{ element: kernelContainer },
|
||||||
{ element: attachToContainer },
|
{ element: attachToContainer },
|
||||||
{ action: addCodeCellButton},
|
{ action: addCodeCellButton },
|
||||||
{ action: addTextCellButton},
|
{ action: addTextCellButton },
|
||||||
{ action: this._trustedAction}
|
{ action: saveNotebookButton },
|
||||||
|
{ action: this._trustedAction }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Primary actions are categorized as those that are added to the 'horizontal' group.
|
||||||
|
// For the vertical toolbar, we can do the same thing and instead use the 'vertical' group.
|
||||||
|
for (let action of primary) {
|
||||||
|
this._actionBar.addAction(action);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async save(): Promise<boolean> {
|
public async save(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
let saved = await this._model.saveModel();
|
let saved = await this._model.saveModel();
|
||||||
|
if (saved) {
|
||||||
|
this.setDirty(false);
|
||||||
|
}
|
||||||
return saved;
|
return saved;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.notificationService.error(localize('saveFailed', 'Failed to save notebook: {0}', notebookUtils.getErrorMessage(err)));
|
this.notificationService.error(localize('saveFailed', 'Failed to save notebook: {0}', notebookUtils.getErrorMessage(err)));
|
||||||
@@ -269,11 +346,46 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setDirty(isDirty: boolean): void {
|
private setDirty(isDirty: boolean): void {
|
||||||
// TODO reenable handling of isDirty
|
if(this._notebookParams.input){
|
||||||
// if (this.editor) {
|
this._notebookParams.input.setDirty(isDirty);
|
||||||
// this.editor.isDirty = isDirty;
|
}
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private actionItemProvider(action: Action): IActionItem {
|
||||||
|
// Check extensions to create ActionItem; otherwise, return undefined
|
||||||
|
// This is similar behavior that exists in MenuItemActionItem
|
||||||
|
if (action instanceof MenuItemAction) {
|
||||||
|
return new LabeledMenuItemActionItem(action, this.keybindingService, this.notificationService, this.contextMenuService, 'notebook-button');
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get notebookParams(): INotebookParams {
|
||||||
|
return this._notebookParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get id(): string {
|
||||||
|
return this._notebookParams.notebookUri.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive(): boolean {
|
||||||
|
return this.editorService.activeEditor === this.notebookParams.input;
|
||||||
|
}
|
||||||
|
|
||||||
|
isVisible(): boolean {
|
||||||
|
let notebookEditor = this.notebookParams.input;
|
||||||
|
return this.editorService.visibleEditors.some(e => e === notebookEditor);
|
||||||
|
}
|
||||||
|
|
||||||
|
isDirty(): boolean {
|
||||||
|
return this.notebookParams.input.isDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
executeEdits(edits: ISingleNotebookEditOperation[]): boolean {
|
||||||
|
if (!edits || edits.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this._model.pushEditOperations(edits);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user