mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-24 11:01:38 -05:00
Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cacf0777c7 | ||
|
|
cb433fbeac | ||
|
|
8f6736df5e | ||
|
|
ef76d730e3 | ||
|
|
aabbeeffa9 | ||
|
|
7a513de543 | ||
|
|
abbd5ec037 | ||
|
|
84009f65ec | ||
|
|
191648df0a | ||
|
|
1265a56ff4 | ||
|
|
2171d44922 | ||
|
|
812f315e69 | ||
|
|
d48258a0fb | ||
|
|
8ec78f67af | ||
|
|
0ac0175bb1 | ||
|
|
f39007cd2d | ||
|
|
348b03327e | ||
|
|
2349aa4df8 | ||
|
|
6497bbcc38 | ||
|
|
a93a173183 | ||
|
|
a1abca26df | ||
|
|
42e55dd2dd | ||
|
|
ca3146d38f | ||
|
|
7f6cd514a5 | ||
|
|
88e24e92b5 | ||
|
|
8b447e361f | ||
|
|
a92dd2d4e4 | ||
|
|
852ec44567 | ||
|
|
b6e32cdeb4 | ||
|
|
4bd264d9be | ||
|
|
4a4b8574d0 | ||
|
|
ded073edd9 | ||
|
|
568f95e7a3 | ||
|
|
5adcabc8de | ||
|
|
e3bce7172c | ||
|
|
96fb618390 | ||
|
|
2d4fdcb661 | ||
|
|
7a84cff5b4 | ||
|
|
2af627b704 | ||
|
|
77fdf18686 | ||
|
|
944a77fe42 | ||
|
|
049678b32e | ||
|
|
3325e4d854 | ||
|
|
1e90e88d4b | ||
|
|
8aeb33c98c | ||
|
|
3b08721835 | ||
|
|
5a30878599 | ||
|
|
c8a8935db0 | ||
|
|
ec196f57bb | ||
|
|
f7809ec3a7 | ||
|
|
71d3ec3616 | ||
|
|
4a7cf8d870 | ||
|
|
4bf8836c0a | ||
|
|
1ca36ee29c | ||
|
|
3446ff88cf | ||
|
|
de5a91a13f | ||
|
|
814cd73019 | ||
|
|
c21611661b | ||
|
|
8f817ce689 | ||
|
|
971b5111e7 | ||
|
|
07069a64ae | ||
|
|
6acea51f12 | ||
|
|
7aa2dab307 | ||
|
|
3091be8f67 | ||
|
|
487531cc52 | ||
|
|
58bfcb4273 | ||
|
|
8d8be27f22 | ||
|
|
27a978cba5 | ||
|
|
71b4e6afa4 | ||
|
|
e1f3b19c0c | ||
|
|
649c2aa5a6 | ||
|
|
cac8cc99e1 | ||
|
|
cb162b16f2 | ||
|
|
86e54ce145 | ||
|
|
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 |
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,6 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- Please search existing issues to avoid creating duplicates. -->
|
<!-- Please search existing issues to avoid creating duplicates. -->
|
||||||
|
|||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: feature request
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution or feature you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export class AlertData implements IAgentDialogData {
|
|||||||
this.eventDescriptionKeyword = alertInfo.eventDescriptionKeyword;
|
this.eventDescriptionKeyword = alertInfo.eventDescriptionKeyword;
|
||||||
this.eventSource = alertInfo.eventSource;
|
this.eventSource = alertInfo.eventSource;
|
||||||
this.hasNotification = alertInfo.hasNotification;
|
this.hasNotification = alertInfo.hasNotification;
|
||||||
this.includeEventDescription = alertInfo.includeEventDescription.toString();
|
this.includeEventDescription = alertInfo.includeEventDescription ? alertInfo.includeEventDescription.toString() : null;
|
||||||
this.isEnabled = alertInfo.isEnabled;
|
this.isEnabled = alertInfo.isEnabled;
|
||||||
this.jobId = alertInfo.jobId;
|
this.jobId = alertInfo.jobId;
|
||||||
this.lastOccurrenceDate = alertInfo.lastOccurrenceDate;
|
this.lastOccurrenceDate = alertInfo.lastOccurrenceDate;
|
||||||
@@ -82,7 +82,7 @@ export class AlertData implements IAgentDialogData {
|
|||||||
this.databaseName = alertInfo.databaseName;
|
this.databaseName = alertInfo.databaseName;
|
||||||
this.countResetDate = alertInfo.countResetDate;
|
this.countResetDate = alertInfo.countResetDate;
|
||||||
this.categoryName = alertInfo.categoryName;
|
this.categoryName = alertInfo.categoryName;
|
||||||
this.alertType = alertInfo.alertType.toString();
|
this.alertType = alertInfo.alertType ? alertInfo.alertType.toString() : null;
|
||||||
this.wmiEventNamespace = alertInfo.wmiEventNamespace;
|
this.wmiEventNamespace = alertInfo.wmiEventNamespace;
|
||||||
this.wmiEventQuery = alertInfo.wmiEventQuery;
|
this.wmiEventQuery = alertInfo.wmiEventQuery;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -1,9 +1,50 @@
|
|||||||
# Microsoft SQL Server Import for Azure Data Studio
|
# Microsoft SQL Server Import for Azure Data Studio
|
||||||
|
|
||||||
Microsoft SQL Server Import for Azure Data Studio is a simple way to copy data from a flat file (.csv, .txt, .json) to a SQL Server table. Checkout below the reasons for using the Import Flat File wizard, how to find this wizard, and a simple example.
|
Microsoft SQL Server Import for Azure Data Studio includes two wizards:
|
||||||
|
- [Import Flat File Wizard](#import-flat-file-wizard-preview)
|
||||||
|
- [Data-tier Application Wizard.](#data-tier-application-wizard-preview)
|
||||||
|
|
||||||
|
## Import Flat File Wizard *(preview)*
|
||||||
|
**The Import Flat File Wizard** is a simple way to copy data from a flat file (.csv, .txt, .json) to a SQL Server table. Checkout below the reasons for using the Import Flat File wizard, how to find this wizard, and a simple example.
|
||||||
|
|
||||||
|
This experience is currently in its initial preview. Please report issues and feature requests [here.](https://github.com/microsoft/azuredatastudio/issues)
|
||||||
|
|
||||||
<img src="https://user-images.githubusercontent.com/30873802/43433347-c958ed28-942b-11e8-8bbc-f4f2529c3978.png" width="800px" />
|
<img src="https://user-images.githubusercontent.com/30873802/43433347-c958ed28-942b-11e8-8bbc-f4f2529c3978.png" width="800px" />
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
* This wizard requires an active connection to a SQL Server instance to start.
|
||||||
|
* This wizard only works on .txt and .csv files.
|
||||||
|
|
||||||
|
### How do I start the Import Flat File wizard?
|
||||||
|
* The main entry point for the wizard is to right click a database in the Object Explorer, and click **Import wizard**.
|
||||||
|
* If a user is connected to a SQL Server instance, the user can also press **Ctrl**+**I** to start the wizard.
|
||||||
|
|
||||||
|
### Why would I use the Import Flat File wizard?
|
||||||
|
This wizard was created to improve the current import experience leveraging an intelligent framework known as Program Synthesis using Examples ([PROSE](https://microsoft.github.io/prose/)). For a user without specialized domain knowledge, importing data can often be a complex, error prone, and tedious task. This wizard streamlines the import process as simple as selecting an input file and unique table name, and the PROSE framework handles the rest.
|
||||||
|
|
||||||
|
PROSE analyzes data patterns in your input file to infer column names, types, delimiters, and more. This framework learns the structure of the file and does all of the hard work so users don't have to.
|
||||||
|
|
||||||
|
Please note that the PROSE binary components used by this extension are licensed under the [MICROSOFT SQL TOOLS IMPORT FLAT FILE EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/extensions/import/Microsoft_SQL_Server_Import_Extension_and_Tools_Import_Flat_File_Preview.docx).
|
||||||
|
|
||||||
|
## Data-tier Application Wizard *(preview)*
|
||||||
|
**The Data-tier Application Wizard** provides an easy to use experience to deploy and extract .dacpac files and import and export .bacpac files.
|
||||||
|
|
||||||
|
This experience is currently in its initial preview. Please report issues and feature requests [here.](https://github.com/microsoft/azuredatastudio/issues)
|
||||||
|
|
||||||
|
<img src="https://user-images.githubusercontent.com/30873802/49676289-f2df6880-fa2d-11e8-8bfa-6213b7734075.png" width="800px" />
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
* This wizard requires an active connection to a SQL Server instance to start.
|
||||||
|
|
||||||
|
### How do I start the Data-tier Application wizard?
|
||||||
|
* The main entry point for the wizard is to right click a database in the Object Explorer, and click **Data-tier Application wizard**.
|
||||||
|
* If a user is connected to a SQL Server instance, the user can also start the wizard from the command palette (Ctrl+Shift+P) by searching for **Data-tier Application wizard.**
|
||||||
|
|
||||||
|
### Why would I use the Data-tier Application wizard?
|
||||||
|
This wizard was created to add the ability to extract and deploy .dacpac files and import and export .bacpac files in Azure Data Studio.
|
||||||
|
|
||||||
|
To learn more about Data-Tier Applications and working with dacpac and bacpac files, [you can read more here.](https://docs.microsoft.com/en-us/sql/relational-databases/data-tier-applications/data-tier-applications?view=sql-server-2017)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
@@ -12,21 +53,6 @@ Licensed under the [MICROSOFT SQL SERVER IMPORT EXTENSION EULA](https://raw.gith
|
|||||||
|
|
||||||
> Note: Microsoft SQL Server Import for Azure Data Studio extension contains the Microsoft SQL Tools Import Flat File component which is also licensed under the above EULA.
|
> Note: Microsoft SQL Server Import for Azure Data Studio extension contains the Microsoft SQL Tools Import Flat File component which is also licensed under the above EULA.
|
||||||
|
|
||||||
## Requirements
|
|
||||||
* This wizard requires an active connection to a SQL Server instance to start.
|
|
||||||
* This wizard only works on .txt and .csv files.
|
|
||||||
|
|
||||||
## How do I start the Flat File Import wizard?
|
|
||||||
* The main entry point for the wizard is to right click a database in the Object Explorer, and click **Import wizard**.
|
|
||||||
* If a user is connected to a SQL Server instance, the user can also press **Ctrl**+**I** to start the wizard.
|
|
||||||
|
|
||||||
## Why would I use the Flat File Import wizard?
|
|
||||||
This wizard was created to improve the current import experience leveraging an intelligent framework known as Program Synthesis using Examples ([PROSE](https://microsoft.github.io/prose/)). For a user without specialized domain knowledge, importing data can often be a complex, error prone, and tedious task. This wizard streamlines the import process as simple as selecting an input file and unique table name, and the PROSE framework handles the rest.
|
|
||||||
|
|
||||||
PROSE analyzes data patterns in your input file to infer column names, types, delimiters, and more. This framework learns the structure of the file and does all of the hard work so users don't have to.
|
|
||||||
|
|
||||||
Please note that the PROSE binary components used by this extension are licensed under the [MICROSOFT SQL TOOLS IMPORT FLAT FILE EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/extensions/import/Microsoft_SQL_Server_Import_Extension_and_Tools_Import_Flat_File_Preview.docx).
|
|
||||||
|
|
||||||
## Code of Conduct
|
## 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.
|
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "import",
|
"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;
|
||||||
|
}
|
||||||
|
|||||||
252
extensions/import/src/wizard/dataTierApplicationWizard.ts
Normal file
252
extensions/import/src/wizard/dataTierApplicationWizard.ts
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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(this.model.server.providerName);
|
||||||
|
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
|
||||||
|
|
||||||
|
let result = await service.deployDacpac(this.model.filePath, this.model.database, this.model.upgradeExisting, ownerUri, sqlops.TaskExecutionMode.execute);
|
||||||
|
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(this.model.server.providerName);
|
||||||
|
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
|
||||||
|
|
||||||
|
let result = await service.extractDacpac(this.model.database, this.model.filePath, this.model.database, this.model.version, ownerUri, sqlops.TaskExecutionMode.execute);
|
||||||
|
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(this.model.server.providerName);
|
||||||
|
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
|
||||||
|
|
||||||
|
let result = await service.exportBacpac(this.model.database, this.model.filePath, ownerUri, sqlops.TaskExecutionMode.execute);
|
||||||
|
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(this.model.server.providerName);
|
||||||
|
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
|
||||||
|
|
||||||
|
let result = await service.importBacpac(this.model.filePath, this.model.database, ownerUri, sqlops.TaskExecutionMode.execute);
|
||||||
|
if (!result || !result.success) {
|
||||||
|
vscode.window.showErrorMessage(
|
||||||
|
localize('alertData.importErrorMessage', "Import failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async getService(providerName: string): Promise<sqlops.DacFxServicesProvider> {
|
||||||
|
let service = sqlops.dataprotocol.getProvider<sqlops.DacFxServicesProvider>(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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
191
extensions/import/src/wizard/pages/deployConfigPage.ts
Normal file
191
extensions/import/src/wizard/pages/deployConfigPage.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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();
|
||||||
|
|
||||||
|
//set the database to the first dropdown value if upgrading, otherwise it should get set to the textbox value
|
||||||
|
if (this.model.upgradeExisting) {
|
||||||
|
this.model.database = values[0].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.9",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,8 @@
|
|||||||
"Microsoft.server-report",
|
"Microsoft.server-report",
|
||||||
"Microsoft.sql-vnext",
|
"Microsoft.sql-vnext",
|
||||||
"Microsoft.whoisactive",
|
"Microsoft.whoisactive",
|
||||||
"Redgate.sql-search"
|
"Redgate.sql-search",
|
||||||
|
"IDERA.sqldm-performance-insights"
|
||||||
],
|
],
|
||||||
"extensionsGallery": {
|
"extensionsGallery": {
|
||||||
"serviceUrl": "https://sqlopsextensions.blob.core.windows.net/marketplace/v1/extensionsGallery.json"
|
"serviceUrl": "https://sqlopsextensions.blob.core.windows.net/marketplace/v1/extensionsGallery.json"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { IDisposableDataProvider } from 'sql/base/browser/ui/table/interfaces';
|
import { IDisposableDataProvider } from 'sql/base/browser/ui/table/interfaces';
|
||||||
|
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||||
|
|
||||||
export interface IObservableCollection<T> {
|
export interface IObservableCollection<T> {
|
||||||
getLength(): number;
|
getLength(): number;
|
||||||
@@ -14,16 +15,12 @@ export interface IObservableCollection<T> {
|
|||||||
dispose(): void;
|
dispose(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoadCancellationToken {
|
|
||||||
isCancelled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DataWindow<T> {
|
class DataWindow<T> {
|
||||||
private _data: T[];
|
private _data: T[];
|
||||||
private _length: number = 0;
|
private _length: number = 0;
|
||||||
private _offsetFromDataSource: number = -1;
|
private _offsetFromDataSource: number = -1;
|
||||||
|
|
||||||
private lastLoadCancellationToken: LoadCancellationToken;
|
private cancellationToken = new CancellationTokenSource();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private loadFunction: (offset: number, count: number) => Thenable<T[]>,
|
private loadFunction: (offset: number, count: number) => Thenable<T[]>,
|
||||||
@@ -36,9 +33,7 @@ class DataWindow<T> {
|
|||||||
this.loadFunction = undefined;
|
this.loadFunction = undefined;
|
||||||
this.placeholderItemGenerator = undefined;
|
this.placeholderItemGenerator = undefined;
|
||||||
this.loadCompleteCallback = undefined;
|
this.loadCompleteCallback = undefined;
|
||||||
if (this.lastLoadCancellationToken) {
|
this.cancellationToken.cancel();
|
||||||
this.lastLoadCancellationToken.isCancelled = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getStartIndex(): number {
|
public getStartIndex(): number {
|
||||||
@@ -65,17 +60,16 @@ class DataWindow<T> {
|
|||||||
this._length = length;
|
this._length = length;
|
||||||
this._data = undefined;
|
this._data = undefined;
|
||||||
|
|
||||||
if (this.lastLoadCancellationToken) {
|
this.cancellationToken.cancel();
|
||||||
this.lastLoadCancellationToken.isCancelled = true;
|
this.cancellationToken = new CancellationTokenSource();
|
||||||
}
|
const currentCancellation = this.cancellationToken;
|
||||||
|
|
||||||
if (length === 0) {
|
if (length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastLoadCancellationToken = new LoadCancellationToken();
|
|
||||||
this.loadFunction(offset, length).then(data => {
|
this.loadFunction(offset, length).then(data => {
|
||||||
if (!this.lastLoadCancellationToken.isCancelled) {
|
if (!currentCancellation.token.isCancellationRequested) {
|
||||||
this._data = data;
|
this._data = data;
|
||||||
this.loadCompleteCallback(this._offsetFromDataSource, this._offsetFromDataSource + this._length);
|
this.loadCompleteCallback(this._offsetFromDataSource, this._offsetFromDataSource + this._length);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export class TableDataView<T extends Slick.SlickData> implements IDisposableData
|
|||||||
this._onRowCountChange.fire();
|
this._onRowCountChange.fire();
|
||||||
}
|
}
|
||||||
|
|
||||||
find(exp: string): Thenable<IFindPosition> {
|
find(exp: string, maxMatches: number = 0): Thenable<IFindPosition> {
|
||||||
if (!this._findFn) {
|
if (!this._findFn) {
|
||||||
return TPromise.wrapError(new Error('no find function provided'));
|
return TPromise.wrapError(new Error('no find function provided'));
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,8 @@ export class TableDataView<T extends Slick.SlickData> implements IDisposableData
|
|||||||
this._onFindCountChange.fire(this._findArray.length);
|
this._onFindCountChange.fire(this._findArray.length);
|
||||||
if (exp) {
|
if (exp) {
|
||||||
this._findObs = Observable.create((observer: Observer<IFindPosition>) => {
|
this._findObs = Observable.create((observer: Observer<IFindPosition>) => {
|
||||||
this._data.forEach((item, i) => {
|
for (let i = 0; i < this._data.length; i++) {
|
||||||
|
let item = this._data[i];
|
||||||
let result = this._findFn(item, exp);
|
let result = this._findFn(item, exp);
|
||||||
if (result) {
|
if (result) {
|
||||||
result.forEach(pos => {
|
result.forEach(pos => {
|
||||||
@@ -96,8 +97,11 @@ export class TableDataView<T extends Slick.SlickData> implements IDisposableData
|
|||||||
observer.next(index);
|
observer.next(index);
|
||||||
this._onFindCountChange.fire(this._findArray.length);
|
this._onFindCountChange.fire(this._findArray.length);
|
||||||
});
|
});
|
||||||
|
if (maxMatches > 0 && this._findArray.length > maxMatches) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
return this._findObs.take(1).toPromise().then(() => {
|
return this._findObs.take(1).toPromise().then(() => {
|
||||||
return this._findArray[this._findIndex];
|
return this._findArray[this._findIndex];
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -5,9 +5,8 @@
|
|||||||
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { Registry } from 'vs/platform/registry/common/platform';
|
|
||||||
import { EditorInput, IEditorInput } from 'vs/workbench/common/editor';
|
import { EditorInput, IEditorInput } from 'vs/workbench/common/editor';
|
||||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||||
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||||
import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput';
|
import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput';
|
||||||
import URI from 'vs/base/common/uri';
|
import URI from 'vs/base/common/uri';
|
||||||
@@ -17,8 +16,9 @@ import { QueryInput } from 'sql/parts/query/common/queryInput';
|
|||||||
import { IQueryEditorOptions } from 'sql/parts/query/common/queryEditorService';
|
import { IQueryEditorOptions } from 'sql/parts/query/common/queryEditorService';
|
||||||
import { QueryPlanInput } from 'sql/parts/queryPlan/queryPlanInput';
|
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 { DEFAULT_NOTEBOOK_PROVIDER, INotebookService } from 'sql/services/notebook/notebookService';
|
||||||
import { DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService';
|
import { getProviderForFileName } from 'sql/parts/notebook/notebookUtils';
|
||||||
|
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ export const untitledFilePrefix = 'SQLQuery';
|
|||||||
|
|
||||||
// mode identifier for SQL mode
|
// mode identifier for SQL mode
|
||||||
export const sqlModeId = 'sql';
|
export const sqlModeId = 'sql';
|
||||||
|
export const notebookModeId = 'notebook';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the specified input is supported by one our custom input types, and if so convert it
|
* Checks if the specified input is supported by one our custom input types, and if so convert it
|
||||||
@@ -58,20 +59,20 @@ export function convertEditorInput(input: EditorInput, options: IQueryEditorOpti
|
|||||||
|
|
||||||
//Notebook
|
//Notebook
|
||||||
let notebookValidator = instantiationService.createInstance(NotebookInputValidator);
|
let notebookValidator = instantiationService.createInstance(NotebookInputValidator);
|
||||||
uri = getNotebookEditorUri(input);
|
uri = getNotebookEditorUri(input, instantiationService);
|
||||||
if(uri && notebookValidator.isNotebookEnabled()){
|
if (uri && notebookValidator.isNotebookEnabled()) {
|
||||||
//TODO: We need to pass in notebook data either through notebook input or notebook service
|
return withService<INotebookService, NotebookInput>(instantiationService, INotebookService, notebookService => {
|
||||||
let fileName: string = 'untitled';
|
let fileName: string = 'untitled';
|
||||||
let providerId: string = DEFAULT_NOTEBOOK_PROVIDER;
|
let providerId: string = DEFAULT_NOTEBOOK_PROVIDER;
|
||||||
if (input) {
|
if (input) {
|
||||||
fileName = input.getName();
|
fileName = input.getName();
|
||||||
providerId = getProviderForFileName(fileName);
|
providerId = getProviderForFileName(fileName, notebookService);
|
||||||
}
|
}
|
||||||
let notebookInputModel = new NotebookInputModel(uri, undefined, false, undefined);
|
let notebookInputModel = new NotebookInputModel(uri, undefined, false, undefined);
|
||||||
notebookInputModel.providerId = providerId;
|
notebookInputModel.providerId = providerId;
|
||||||
//TO DO: Second parameter has to be the content.
|
let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel);
|
||||||
let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel);
|
return notebookInput;
|
||||||
return notebookInput;
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return input;
|
return input;
|
||||||
@@ -96,6 +97,13 @@ export function getSupportedInputResource(input: IEditorInput): URI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input instanceof ResourceEditorInput) {
|
||||||
|
let resourceCast: ResourceEditorInput = <ResourceEditorInput>input;
|
||||||
|
if (resourceCast) {
|
||||||
|
return resourceCast.getResource();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,46 +161,51 @@ function getQueryPlanEditorUri(input: EditorInput): URI {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If input is a supported notebook editor file (.ipynb), return it's URI. Otherwise return undefined.
|
* If input is a supported notebook editor file (.ipynb), return it's URI. Otherwise return undefined.
|
||||||
* @param input The EditorInput to get the URI of.
|
* @param input The EditorInput to get the URI of.
|
||||||
*/
|
*/
|
||||||
function getNotebookEditorUri(input: EditorInput): URI {
|
function getNotebookEditorUri(input: EditorInput, instantiationService: IInstantiationService): URI {
|
||||||
if (!input || !input.getName()) {
|
if (!input || !input.getName()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// If this editor is not already of type notebook input
|
// If this editor is not already of type notebook input
|
||||||
if (!(input instanceof NotebookInput)) {
|
if (!(input instanceof NotebookInput)) {
|
||||||
let uri: URI = getSupportedInputResource(input);
|
let uri: URI = getSupportedInputResource(input);
|
||||||
if (uri) {
|
if (uri) {
|
||||||
if (hasFileExtension(getNotebookFileExtensions(), input, false)) {
|
if (hasFileExtension(getNotebookFileExtensions(instantiationService), input, false) || hasNotebookFileMode(input)) {
|
||||||
return uri;
|
return uri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNotebookFileExtensions() {
|
function getNotebookFileExtensions(instantiationService: IInstantiationService): string[] {
|
||||||
let notebookRegistry = Registry.as<INotebookProviderRegistry>(Extensions.NotebookProviderContribution);
|
return withService<INotebookService, string[]>(instantiationService, INotebookService, notebookService => {
|
||||||
return notebookRegistry.getSupportedFileExtensions();
|
return notebookService.getSupportedFileExtensions();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProviderForFileName(fileName: string) {
|
/**
|
||||||
let fileExt = path.extname(fileName);
|
* Checks whether the given EditorInput is set to either undefined or notebook mode
|
||||||
if (fileExt && fileExt.startsWith('.')) {
|
* @param input The EditorInput to check the mode of
|
||||||
fileExt = fileExt.slice(1,fileExt.length);
|
*/
|
||||||
let notebookRegistry = Registry.as<INotebookProviderRegistry>(Extensions.NotebookProviderContribution);
|
function hasNotebookFileMode(input: EditorInput): boolean {
|
||||||
return notebookRegistry.getProviderForFileType(fileExt);
|
if (input instanceof UntitledEditorInput) {
|
||||||
|
let untitledCast: UntitledEditorInput = <UntitledEditorInput>input;
|
||||||
|
return (untitledCast && untitledCast.getModeId() === notebookModeId);
|
||||||
}
|
}
|
||||||
return DEFAULT_NOTEBOOK_PROVIDER;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withService<TService, TResult>(instantiationService: IInstantiationService, serviceId: ServiceIdentifier<TService>, action: (service: TService) => TResult, ): TResult {
|
||||||
|
return instantiationService.invokeFunction(accessor => {
|
||||||
|
let service = accessor.get(serviceId);
|
||||||
|
return action(service);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the given EditorInput is set to either undefined or sql mode
|
* Checks whether the given EditorInput is set to either undefined or sql mode
|
||||||
@@ -229,3 +242,17 @@ function hasFileExtension(extensions: string[], input: EditorInput, checkUntitle
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns file mode - notebookModeId or sqlModeId
|
||||||
|
export function getFileMode(instantiationService: IInstantiationService, resource: URI): string {
|
||||||
|
if (!resource) {
|
||||||
|
return sqlModeId;
|
||||||
|
}
|
||||||
|
return withService<INotebookService, string>(instantiationService, INotebookService, notebookService => {
|
||||||
|
for (const editor of notebookService.listNotebookEditors()) {
|
||||||
|
if (editor.notebookParams.notebookUri === resource) {
|
||||||
|
return notebookModeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sqlModeId;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
@@ -89,6 +90,8 @@
|
|||||||
margin: 0px 13px;
|
margin: 0px 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vs-dark .connection-dialog .connection-history-actions .action-label.icon,
|
||||||
|
.hc-black .connection-dialog .connection-history-actions .action-label.icon,
|
||||||
.connection-dialog .connection-history-actions .action-label.icon {
|
.connection-dialog .connection-history-actions .action-label.icon {
|
||||||
display: block;
|
display: block;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
@@ -104,14 +107,32 @@
|
|||||||
background: url('clear-search-results.svg');
|
background: url('clear-search-results.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
.vs-dark .search-action.clear-search-results,
|
.vs-dark .search-action.clear-search-results,
|
||||||
.hc-black .search-action.clear-search-results {
|
.hc-black .search-action.clear-search-results {
|
||||||
background: url('clear-search-results-dark.svg');
|
background: url('clear-search-results-dark.svg');
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
.connection-details-title {
|
.connection-details-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -168,8 +168,7 @@ export const DashboardModule = (params, selector: string, instantiationService:
|
|||||||
if (e instanceof NavigationEnd) {
|
if (e instanceof NavigationEnd) {
|
||||||
this.navigations++;
|
this.navigations++;
|
||||||
TelemetryUtils.addTelemetry(this.telemetryService, TelemetryKeys.DashboardNavigated, {
|
TelemetryUtils.addTelemetry(this.telemetryService, TelemetryKeys.DashboardNavigated, {
|
||||||
numberOfNavigations: this.navigations,
|
numberOfNavigations: this.navigations
|
||||||
routeUrl: e.url
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,13 +48,13 @@ export function textFormatter(row: number, cell: any, value: any, columnDef: any
|
|||||||
if (!value.isNull) {
|
if (!value.isNull) {
|
||||||
valueToDisplay = value.displayValue.replace(/(\r\n|\n|\r)/g, ' ');
|
valueToDisplay = value.displayValue.replace(/(\r\n|\n|\r)/g, ' ');
|
||||||
valueToDisplay = escape(valueToDisplay.length > 250 ? valueToDisplay.slice(0, 250) + '...' : valueToDisplay);
|
valueToDisplay = escape(valueToDisplay.length > 250 ? valueToDisplay.slice(0, 250) + '...' : valueToDisplay);
|
||||||
titleValue = value.displayValue;
|
titleValue = valueToDisplay;
|
||||||
} else {
|
} else {
|
||||||
cellClasses += ' missing-value';
|
cellClasses += ' missing-value';
|
||||||
}
|
}
|
||||||
} else if (typeof value === 'string') {
|
} else if (typeof value === 'string') {
|
||||||
valueToDisplay = escape(value.length > 250 ? value.slice(0, 250) + '...' : value);
|
valueToDisplay = escape(value.length > 250 ? value.slice(0, 250) + '...' : value);
|
||||||
titleValue = value;
|
titleValue = valueToDisplay;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<span title="${titleValue}" class="${cellClasses}">${valueToDisplay}</span>`;
|
return `<span title="${titleValue}" class="${cellClasses}">${valueToDisplay}</span>`;
|
||||||
|
|||||||
@@ -343,6 +343,9 @@ export class EditDataComponent extends GridParentComponent implements OnInit, On
|
|||||||
handleResultSet(self: EditDataComponent, event: any): void {
|
handleResultSet(self: EditDataComponent, event: any): void {
|
||||||
// Clone the data before altering it to avoid impacting other subscribers
|
// Clone the data before altering it to avoid impacting other subscribers
|
||||||
let resultSet = Object.assign({}, event.data);
|
let resultSet = Object.assign({}, event.data);
|
||||||
|
if (!resultSet.complete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Add an extra 'new row'
|
// Add an extra 'new row'
|
||||||
resultSet.rowCount++;
|
resultSet.rowCount++;
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import { AlertsViewComponent } from 'sql/parts/jobManagement/views/alertsView.co
|
|||||||
import { OperatorsViewComponent } from 'sql/parts/jobManagement/views/operatorsView.component';
|
import { 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}",
|
||||||
|
|||||||
@@ -304,14 +304,6 @@ table.jobprevruns > tbody {
|
|||||||
background-image: url('refresh_inverse.svg');
|
background-image: url('refresh_inverse.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-actionbar-container .monaco-action-bar > ul.actions-container {
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
jobsview-component .agent-actionbar-container {
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-actionbar-container .monaco-action-bar > ul.actions-container > li.action-item {
|
.agent-actionbar-container .monaco-action-bar > ul.actions-container > li.action-item {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
@@ -414,4 +406,11 @@ jobsview-component .jobview-grid .slick-cell.error-row {
|
|||||||
#proxiesDiv .proxyview-proxynameindicatordisabled {
|
#proxiesDiv .proxyview-proxynameindicatordisabled {
|
||||||
width: 5px;
|
width: 5px;
|
||||||
background: red;
|
background: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
#jobsDiv jobsview-component .monaco-toolbar.carbon-taskbar,
|
||||||
|
#operatorsDiv joboperatorsview-component .monaco-toolbar.carbon-taskbar,
|
||||||
|
#alertsDiv jobalertsview-component .monaco-toolbar.carbon-taskbar,
|
||||||
|
#proxiesDiv jobproxiesview-component .monaco-toolbar.carbon-taskbar {
|
||||||
|
margin: 10px 0px 10px 0px;
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
<!-- Job History details -->
|
<!-- 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,7 +156,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<div #jobsteps style="height: 100%">
|
<div #jobsteps *ngIf="showSteps === true" style="flex: 1 1 auto; position: relative">
|
||||||
<jobstepsview-component *ngIf="showSteps === true"></jobstepsview-component>
|
<jobstepsview-component *ngIf="showSteps === true"></jobstepsview-component>
|
||||||
</div>
|
</div>
|
||||||
<h3 *ngIf="showSteps === false">No Steps Available</h3>
|
<h3 *ngIf="showSteps === false">No Steps Available</h3>
|
||||||
|
|||||||
@@ -23,13 +23,14 @@ import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/work
|
|||||||
import { attachListStyler } from 'vs/platform/theme/common/styler';
|
import { 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';
|
||||||
|
|
||||||
@@ -64,7 +65,6 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
|
|||||||
private _agentJobInfo: sqlops.AgentJobInfo;
|
private _agentJobInfo: sqlops.AgentJobInfo;
|
||||||
private _noJobsAvailable: boolean = false;
|
private _noJobsAvailable: boolean = false;
|
||||||
|
|
||||||
private static readonly INITIAL_TREE_HEIGHT: number = 780;
|
|
||||||
private static readonly HEADING_HEIGHT: number = 24;
|
private static readonly HEADING_HEIGHT: number = 24;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -77,7 +77,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 +142,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 +294,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 +304,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';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
.all-jobs {
|
.all-jobs {
|
||||||
display: inline;
|
display: inline;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
padding-bottom: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-container .overview-tab .resultsViewCollapsible {
|
.overview-container .overview-tab .resultsViewCollapsible {
|
||||||
@@ -177,17 +176,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 +240,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 {
|
||||||
@@ -257,14 +265,22 @@ jobhistory-component > .jobhistory-heading-container > .icon.in-progress {
|
|||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
jobhistory-component > .agent-actionbar-container .monaco-action-bar > ul.actions-container {
|
jobhistory-component > .agent-actionbar-container {
|
||||||
border-top: 3px solid #f4f4f4;
|
border-top: 3px solid #f4f4f4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vs-dark jobhistory-component > .agent-actionbar-container .monaco-action-bar > ul.actions-container {
|
.vs-dark jobhistory-component > .agent-actionbar-container {
|
||||||
border-top: 3px solid #444444;
|
border-top: 3px solid #444444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hc-black jobhistory-component > .agent-actionbar-container .monaco-action-bar > ul.actions-container {
|
.hc-black jobhistory-component > .agent-actionbar-container {
|
||||||
border-top: 3px solid #2b56f2;
|
border-top: 3px solid #2b56f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jobhistory-component .step-table.prev-run-list .monaco-tree-wrapper .monaco-tree-row {
|
||||||
|
width: 96%;
|
||||||
|
}
|
||||||
|
|
||||||
|
jobhistory-component .agent-actionbar-container > .monaco-toolbar.carbon-taskbar {
|
||||||
|
margin: 10px 0px 5px 0px;
|
||||||
|
}
|
||||||
@@ -22,4 +22,6 @@
|
|||||||
</td>
|
</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,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
jobstepsview-component {
|
jobstepsview-component {
|
||||||
padding-top: 10px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
jobstepsview-component .steps-tree .monaco-tree-wrapper .monaco-tree-row {
|
||||||
|
width: 99.2%;
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import * as DOM from 'vs/base/browser/dom';
|
import * 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() {
|
||||||
@@ -587,7 +591,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
|
|||||||
|
|
||||||
private async curateJobHistory(jobs: sqlops.AgentJobInfo[], ownerUri: string) {
|
private async curateJobHistory(jobs: sqlops.AgentJobInfo[], ownerUri: string) {
|
||||||
const self = this;
|
const self = this;
|
||||||
jobs.forEach(async (job) => {
|
await Promise.all(jobs.map(async (job) => {
|
||||||
await this._jobManagementService.getJobHistory(ownerUri, job.jobId, job.name).then(async(result) => {
|
await this._jobManagementService.getJobHistory(ownerUri, job.jobId, job.name).then(async(result) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
self.jobSteps[job.jobId] = result.steps ? result.steps : [];
|
self.jobSteps[job.jobId] = result.steps ? result.steps : [];
|
||||||
@@ -618,32 +622,23 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private createJobChart(jobId: string, jobHistories: sqlops.AgentJobHistoryInfo[]): void {
|
private createJobChart(jobId: string, jobHistories: sqlops.AgentJobHistoryInfo[]): void {
|
||||||
let chartHeights = this.getChartHeights(jobHistories);
|
let chartHeights = this.getChartHeights(jobHistories);
|
||||||
let runCharts = [];
|
let runCharts = [];
|
||||||
for (let i = 0; i < jobHistories.length; i++) {
|
for (let i = 0; i < chartHeights.length; i++) {
|
||||||
let runGraph = $(`table#${jobId}.jobprevruns > tbody > tr > td > div.bar${i}`);
|
let runGraph = $(`table#${jobId}.jobprevruns > tbody > tr > td > div.bar${i}`);
|
||||||
if (jobHistories && jobHistories.length > 0) {
|
runGraph.css('height', chartHeights[i]);
|
||||||
runGraph.css('height', chartHeights[i]);
|
let bgColor = jobHistories[i].runStatus === 0 ? 'red' : 'green';
|
||||||
let bgColor = jobHistories[i].runStatus === 0 ? 'red' : 'green';
|
runGraph.css('background', bgColor);
|
||||||
runGraph.css('background', bgColor);
|
runGraph.hover((e) => {
|
||||||
runGraph.hover((e) => {
|
let currentTarget = e.currentTarget;
|
||||||
let currentTarget = e.currentTarget;
|
currentTarget.title = jobHistories[i].runDuration;
|
||||||
currentTarget.title = jobHistories[i].runDuration;
|
});
|
||||||
});
|
if (runGraph.get(0)) {
|
||||||
if (runGraph.get(0)) {
|
runCharts.push(runGraph.get(0).outerHTML);
|
||||||
runCharts.push(runGraph.get(0).outerHTML);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
runGraph.css('height', '5px');
|
|
||||||
runGraph.css('background', 'red');
|
|
||||||
runGraph.hover((e) => {
|
|
||||||
let currentTarget = e.currentTarget;
|
|
||||||
currentTarget.title = 'Job not run.';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (runCharts.length > 0) {
|
if (runCharts.length > 0) {
|
||||||
@@ -654,7 +649,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
|
|||||||
// chart height normalization logic
|
// chart height normalization logic
|
||||||
private getChartHeights(jobHistories: sqlops.AgentJobHistoryInfo[]): string[] {
|
private getChartHeights(jobHistories: sqlops.AgentJobHistoryInfo[]): string[] {
|
||||||
if (!jobHistories || jobHistories.length === 0) {
|
if (!jobHistories || jobHistories.length === 0) {
|
||||||
return ['5px', '5px', '5px', '5px', '5px'];
|
return [];
|
||||||
}
|
}
|
||||||
let maxDuration: number = 0;
|
let maxDuration: number = 0;
|
||||||
jobHistories.forEach(history => {
|
jobHistories.forEach(history => {
|
||||||
@@ -933,19 +928,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,31 +2,32 @@
|
|||||||
<span *ngIf="hasStatus" class="card-status">
|
<span *ngIf="hasStatus" class="card-status">
|
||||||
<div class="status-content" [style.backgroundColor]="statusColor"></div>
|
<div class="status-content" [style.backgroundColor]="statusColor"></div>
|
||||||
</span>
|
</span>
|
||||||
|
<span *ngIf="showRadioButton" class="selection-indicator-container">
|
||||||
<ng-container *ngIf="isVerticalButton">
|
<div *ngIf="showAsSelected" class="selection-indicator"></div>
|
||||||
<div class="card-vertical-button">
|
</span>
|
||||||
<div *ngIf="iconPath" class="iconContainer">
|
<ng-container *ngIf="isVerticalButton">
|
||||||
<div [class]="iconClass" [style.maxWidth]="iconWidth" [style.maxHeight]="iconHeight"></div>
|
<div class="card-vertical-button">
|
||||||
</div>
|
<div *ngIf="iconPath" class="iconContainer">
|
||||||
<h4 class="card-label">{{label}}</h4>
|
<div [class]="iconClass" [style.maxWidth]="iconWidth" [style.maxHeight]="iconHeight"></div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
<h4 class="card-label">{{label}}</h4>
|
||||||
|
</div>
|
||||||
<ng-container *ngIf="isDetailsCard">
|
</ng-container>
|
||||||
<div class="card-content">
|
|
||||||
<h4 class="card-label">{{label}}</h4>
|
|
||||||
<p class="card-value">{{value}}</p>
|
|
||||||
<span *ngIf="actions">
|
|
||||||
<table class="model-table">
|
|
||||||
<tr *ngFor="let action of actions">
|
|
||||||
<td class="table-row">{{action.label}}</td>
|
|
||||||
<td *ngIf="action.actionTitle" class="table-row">
|
|
||||||
<a class="pointer prominent" (click)="onDidActionClick(action)">{{action.actionTitle}}</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
|
<ng-container *ngIf="isDetailsCard">
|
||||||
|
<div class="card-content">
|
||||||
|
<h4 class="card-label">{{label}}</h4>
|
||||||
|
<p class="card-value">{{value}}</p>
|
||||||
|
<span *ngIf="actions">
|
||||||
|
<table class="model-table">
|
||||||
|
<tr *ngFor="let action of actions">
|
||||||
|
<td class="table-row">{{action.label}}</td>
|
||||||
|
<td *ngIf="action.actionTitle" class="table-row">
|
||||||
|
<a class="pointer prominent" (click)="onDidActionClick(action)">{{action.actionTitle}}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,7 +29,7 @@ export default class CardComponent extends ComponentWithIconBase implements ICom
|
|||||||
|
|
||||||
private backgroundColor: string;
|
private backgroundColor: string;
|
||||||
|
|
||||||
constructor( @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
|
constructor(@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
|
||||||
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
|
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
|
||||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService
|
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService
|
||||||
) {
|
) {
|
||||||
@@ -130,6 +130,14 @@ export default class CardComponent extends ComponentWithIconBase implements ICom
|
|||||||
return this.cardType === 'VerticalButton';
|
return this.cardType === 'VerticalButton';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get showRadioButton():boolean{
|
||||||
|
return this.selectable && (this.selected || this._hasFocus)
|
||||||
|
}
|
||||||
|
|
||||||
|
public get showAsSelected(): boolean {
|
||||||
|
return this.selectable && this.selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public get actions(): ActionDescriptor[] {
|
public get actions(): ActionDescriptor[] {
|
||||||
return this.getPropertyOrDefault<CardProperties, ActionDescriptor[]>((props) => props.actions, []);
|
return this.getPropertyOrDefault<CardProperties, ActionDescriptor[]>((props) => props.actions, []);
|
||||||
@@ -156,6 +164,7 @@ export default class CardComponent extends ComponentWithIconBase implements ICom
|
|||||||
|
|
||||||
private updateTheme(theme: IColorTheme) {
|
private updateTheme(theme: IColorTheme) {
|
||||||
this.backgroundColor = theme.getColor(colors.editorBackground, true).toString();
|
this.backgroundColor = theme.getColor(colors.editorBackground, true).toString();
|
||||||
|
this._changeRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
private onDidActionClick(action: ActionDescriptor): void {
|
private onDidActionClick(action: ActionDescriptor): void {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
.model-card {
|
.model-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -7,23 +6,18 @@
|
|||||||
margin: 15px;
|
margin: 15px;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
box-shadow: rgba(120, 120, 120, 0.75) 0px 0px 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-card.selected {
|
.model-card.selected {
|
||||||
border-color: darkblue
|
border-color: rgb(0, 120, 215);
|
||||||
}
|
box-shadow: rgba(0, 120, 215, 0.75) 0px 0px 6px;
|
||||||
|
|
||||||
.vs-dark .monaco-workbench .model-card.selected,
|
|
||||||
.hc-black .monaco-workbench .model-card.selected {
|
|
||||||
border-color: darkblue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-card.unselected {
|
.model-card.unselected {
|
||||||
border-color: rgb(214, 214, 214);
|
border-color: rgb(214, 214, 214);
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -102,21 +96,43 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.model-card .selection-indicator-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: white;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: rgb(0, 120, 215);
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-card .selection-indicator {
|
||||||
|
margin: 4px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgb(0, 120, 215);
|
||||||
|
}
|
||||||
|
|
||||||
.model-card .model-table {
|
.model-card .model-table {
|
||||||
border-spacing: 5px;
|
border-spacing: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-table .table-row {
|
.model-table .table-row {
|
||||||
width: auto;
|
width: auto;
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-table .table-cell {
|
.model-table .table-cell {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-table a {
|
.model-table a {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: underline
|
text-decoration: underline
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
127
src/sql/parts/notebook/cellToggleMoreActions.ts
Normal file
127
src/sql/parts/notebook/cellToggleMoreActions.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import { ElementRef } from '@angular/core';
|
||||||
|
|
||||||
|
import { nb } from 'sqlops';
|
||||||
|
|
||||||
|
import { localize } from 'vs/nls';
|
||||||
|
import { Action } from 'vs/base/common/actions';
|
||||||
|
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||||
|
import { getErrorMessage } from 'vs/base/common/errors';
|
||||||
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||||
|
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||||
|
|
||||||
|
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||||
|
import { CellContext, CellActionBase } from 'sql/parts/notebook/cellViews/codeActions';
|
||||||
|
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
||||||
|
import { ToggleMoreWidgetAction } from 'sql/parts/dashboard/common/actions';
|
||||||
|
import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts';
|
||||||
|
import { CellModel } from 'sql/parts/notebook/models/cell';
|
||||||
|
|
||||||
|
export class CellToggleMoreActions {
|
||||||
|
private _actions: Action[] = [];
|
||||||
|
private _moreActions: ActionBar;
|
||||||
|
constructor(
|
||||||
|
@IInstantiationService private instantiationService: IInstantiationService) {
|
||||||
|
this._actions.push(
|
||||||
|
instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', 'Delete')),
|
||||||
|
instantiationService.createInstance(AddCellFromContextAction,'codeBefore', localize('codeBefore', 'Insert Code before'), CellTypes.Code, false),
|
||||||
|
instantiationService.createInstance(AddCellFromContextAction, 'codeAfter', localize('codeAfter', 'Insert Code after'), CellTypes.Code, true),
|
||||||
|
instantiationService.createInstance(AddCellFromContextAction, 'markdownBefore', localize('markdownBefore', 'Insert Markdown before'), CellTypes.Markdown, false),
|
||||||
|
instantiationService.createInstance(AddCellFromContextAction, 'markdownAfter', localize('markdownAfter', 'Insert Markdown after'), CellTypes.Markdown, true),
|
||||||
|
instantiationService.createInstance(ClearCellOutputAction, 'clear', localize('clear', 'Clear output'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggle(showIcon: boolean, elementRef: ElementRef, model: NotebookModel, cellModel: ICellModel) {
|
||||||
|
let context = new CellContext(model,cellModel);
|
||||||
|
let moreActionsElement = <HTMLElement>elementRef.nativeElement;
|
||||||
|
if (showIcon) {
|
||||||
|
if (moreActionsElement.childNodes.length > 0) {
|
||||||
|
moreActionsElement.removeChild(moreActionsElement.childNodes[0]);
|
||||||
|
}
|
||||||
|
this._moreActions = new ActionBar(moreActionsElement, { orientation: ActionsOrientation.VERTICAL });
|
||||||
|
this._moreActions.context = { target: moreActionsElement };
|
||||||
|
this._moreActions.push(this.instantiationService.createInstance(ToggleMoreWidgetAction, this._actions, context), { icon: showIcon, label: false });
|
||||||
|
}
|
||||||
|
else if (moreActionsElement.childNodes.length > 0) {
|
||||||
|
moreActionsElement.removeChild(moreActionsElement.childNodes[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AddCellFromContextAction extends CellActionBase {
|
||||||
|
constructor(
|
||||||
|
id: string, label: string, private cellType: CellType, private isAfter: boolean,
|
||||||
|
@INotificationService notificationService: INotificationService
|
||||||
|
) {
|
||||||
|
super(id, label, undefined, notificationService);
|
||||||
|
}
|
||||||
|
|
||||||
|
runCellAction(context: CellContext): Promise<void> {
|
||||||
|
try {
|
||||||
|
let model = context.model;
|
||||||
|
let index = model.cells.findIndex((cell) => cell.id === context.cell.id);
|
||||||
|
if (index !== undefined && this.isAfter) {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
model.addCell(this.cellType, index);
|
||||||
|
} catch (error) {
|
||||||
|
let message = getErrorMessage(error);
|
||||||
|
|
||||||
|
this.notificationService.notify({
|
||||||
|
severity: Severity.Error,
|
||||||
|
message: message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeleteCellAction extends CellActionBase {
|
||||||
|
constructor(id: string, label: string,
|
||||||
|
@INotificationService notificationService: INotificationService
|
||||||
|
) {
|
||||||
|
super(id, label, undefined, notificationService);
|
||||||
|
}
|
||||||
|
|
||||||
|
runCellAction(context: CellContext): Promise<void> {
|
||||||
|
try {
|
||||||
|
context.model.deleteCell(context.cell);
|
||||||
|
} catch (error) {
|
||||||
|
let message = getErrorMessage(error);
|
||||||
|
|
||||||
|
this.notificationService.notify({
|
||||||
|
severity: Severity.Error,
|
||||||
|
message: message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClearCellOutputAction extends CellActionBase {
|
||||||
|
constructor(id: string, label: string,
|
||||||
|
@INotificationService notificationService: INotificationService
|
||||||
|
) {
|
||||||
|
super(id, label, undefined, notificationService);
|
||||||
|
}
|
||||||
|
|
||||||
|
runCellAction(context: CellContext): Promise<void> {
|
||||||
|
try {
|
||||||
|
(context.model.activeCell as CellModel).clearOutputs();
|
||||||
|
} catch (error) {
|
||||||
|
let message = getErrorMessage(error);
|
||||||
|
|
||||||
|
this.notificationService.notify({
|
||||||
|
severity: Severity.Error,
|
||||||
|
message: message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -9,6 +9,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<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="moreActions" style="flex: 0 0 auto; display: flex; flex-flow:column;width: 20px; min-height: 20px; max-height: 20px; padding-top: 0px; orientation: portrait">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,11 +4,16 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
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, 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';
|
||||||
import { QueryTextEditor } from 'sql/parts/modelComponents/queryTextEditor';
|
import { QueryTextEditor } from 'sql/parts/modelComponents/queryTextEditor';
|
||||||
|
import { CellToggleMoreActions } from 'sql/parts/notebook/cellToggleMoreActions';
|
||||||
|
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
||||||
|
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||||
|
import { RunCellAction, CellContext } from 'sql/parts/notebook/cellViews/codeActions';
|
||||||
|
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
||||||
|
|
||||||
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
import { 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';
|
||||||
@@ -19,20 +24,11 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
|
|||||||
import { ITextModel } from 'vs/editor/common/model';
|
import { ITextModel } from 'vs/editor/common/model';
|
||||||
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||||
import URI from 'vs/base/common/uri';
|
import URI from 'vs/base/common/uri';
|
||||||
import { localize } from 'vs/nls';
|
|
||||||
import { Action } from 'vs/base/common/actions';
|
|
||||||
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
|
||||||
import { Schemas } from 'vs/base/common/network';
|
import { Schemas } from 'vs/base/common/network';
|
||||||
import * as DOM from 'vs/base/browser/dom';
|
import * as DOM from 'vs/base/browser/dom';
|
||||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||||
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||||
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
|
|
||||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
|
||||||
import { RunCellAction, DeleteCellAction, AddCellAction, CellContext } from 'sql/parts/notebook/cellViews/codeActions';
|
|
||||||
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
|
|
||||||
import { ToggleMoreWidgetAction } from 'sql/parts/dashboard/common/actions';
|
|
||||||
import { CellTypes } from 'sql/parts/notebook/models/contracts';
|
|
||||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||||
|
|
||||||
export const CODE_SELECTOR: string = 'code-component';
|
export const CODE_SELECTOR: string = 'code-component';
|
||||||
@@ -41,11 +37,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,14 +50,19 @@ 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;
|
|
||||||
private readonly _minimumHeight = 30;
|
private readonly _minimumHeight = 30;
|
||||||
private _editor: QueryTextEditor;
|
private _editor: QueryTextEditor;
|
||||||
private _editorInput: UntitledEditorInput;
|
private _editorInput: UntitledEditorInput;
|
||||||
private _editorModel: ITextModel;
|
private _editorModel: ITextModel;
|
||||||
private _uri: string;
|
private _uri: string;
|
||||||
private _model: NotebookModel;
|
private _model: NotebookModel;
|
||||||
|
private _activeCellId: string;
|
||||||
|
private _cellToggleMoreActions: CellToggleMoreActions;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
|
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
|
||||||
@@ -74,17 +76,32 @@ export class CodeComponent extends AngularDisposable implements OnInit {
|
|||||||
@Inject(INotificationService) private notificationService: INotificationService,
|
@Inject(INotificationService) private notificationService: INotificationService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
this._cellToggleMoreActions = this._instantiationService.createInstance(CellToggleMoreActions);
|
||||||
}
|
}
|
||||||
|
|
||||||
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._cellToggleMoreActions.toggle(true, this.moreActionsElementRef, this.model, this.cellModel);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._cellToggleMoreActions.toggle(false, this.moreActionsElementRef, this.model, this.cellModel);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterContentInit(): void {
|
ngAfterContentInit(): void {
|
||||||
@@ -98,6 +115,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,21 +160,9 @@ export class CodeComponent extends AngularDisposable implements OnInit {
|
|||||||
this._actionBar.setContent([
|
this._actionBar.setContent([
|
||||||
{ action: runCellAction }
|
{ action: runCellAction }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let moreActionsElement = <HTMLElement>this.moreactionsElement.nativeElement;
|
|
||||||
this._moreActions = new ActionBar(moreActionsElement, { orientation: ActionsOrientation.VERTICAL });
|
|
||||||
this._moreActions.context = { target: moreActionsElement };
|
|
||||||
|
|
||||||
let actions: Action[] = [];
|
|
||||||
actions.push(this._instantiationService.createInstance(AddCellAction, 'codeBefore', localize('codeBefore', 'Insert Code before'), CellTypes.Code, false));
|
|
||||||
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));
|
|
||||||
actions.push(this._instantiationService.createInstance(AddCellAction, 'markdownAfter', localize('markdownAfter', 'Insert Markdown after'), CellTypes.Markdown, true));
|
|
||||||
actions.push(this._instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', 'Delete')));
|
|
||||||
|
|
||||||
this._moreActions.push(this._instantiationService.createInstance(ToggleMoreWidgetAction, actions, context), { icon: true, label: false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private createUri(): URI {
|
private createUri(): URI {
|
||||||
let uri = URI.from({ scheme: Schemas.untitled, path: `notebook-editor-${this.cellModel.id}` });
|
let uri = URI.from({ scheme: Schemas.untitled, path: `notebook-editor-${this.cellModel.id}` });
|
||||||
// Use this to set the internal (immutable) and public (shared with extension) uri properties
|
// Use this to set the internal (immutable) and public (shared with extension) uri properties
|
||||||
@@ -180,8 +189,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 {
|
||||||
@@ -46,10 +45,6 @@ code-component .carbon-taskbar .icon {
|
|||||||
width: 40px;
|
width: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
code-component .action-label.icon.toggle-more {
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
code-component .carbon-taskbar.monaco-toolbar .monaco-action-bar.animated .actions-container
|
code-component .carbon-taskbar.monaco-toolbar .monaco-action-bar.animated .actions-container
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export class CellContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class CellActionBase extends Action {
|
export abstract class CellActionBase extends Action {
|
||||||
|
|
||||||
constructor(id: string, label: string, icon: string, protected notificationService: INotificationService) {
|
constructor(id: string, label: string, icon: string, protected notificationService: INotificationService) {
|
||||||
super(id, label, icon);
|
super(id, label, icon);
|
||||||
@@ -135,53 +135,3 @@ export class RunCellAction extends ToggleableAction {
|
|||||||
return clientSession.kernel;
|
return clientSession.kernel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AddCellAction extends CellActionBase {
|
|
||||||
constructor(
|
|
||||||
id: string, label: string, private cellType: CellType, private isAfter: boolean,
|
|
||||||
@INotificationService notificationService: INotificationService
|
|
||||||
) {
|
|
||||||
super(id, label, undefined, notificationService);
|
|
||||||
}
|
|
||||||
|
|
||||||
runCellAction(context: CellContext): Promise<void> {
|
|
||||||
try {
|
|
||||||
let model = context.model;
|
|
||||||
let index = model.cells.findIndex((cell) => cell.id === context.cell.id);
|
|
||||||
if (index !== undefined && this.isAfter) {
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
model.addCell(this.cellType, index);
|
|
||||||
} catch (error) {
|
|
||||||
let message = getErrorMessage(error);
|
|
||||||
|
|
||||||
this.notificationService.notify({
|
|
||||||
severity: Severity.Error,
|
|
||||||
message: message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DeleteCellAction extends CellActionBase {
|
|
||||||
constructor(id: string, label: string,
|
|
||||||
@INotificationService notificationService: INotificationService
|
|
||||||
) {
|
|
||||||
super(id, label, undefined, notificationService);
|
|
||||||
}
|
|
||||||
|
|
||||||
runCellAction(context: CellContext): Promise<void> {
|
|
||||||
try {
|
|
||||||
context.model.deleteCell(context.cell);
|
|
||||||
} catch (error) {
|
|
||||||
let message = getErrorMessage(error);
|
|
||||||
|
|
||||||
this.notificationService.notify({
|
|
||||||
severity: Severity.Error,
|
|
||||||
message: message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user