Compare commits

...

63 Commits
1.3.3 ... 1.3.7

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

* fixed width for scrollbar

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

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

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

* generalized feature telemetry via dialogs

* renamed eventName property to dialogName

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

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

* agent fixes

* fit step tree

* fix rendering issue

* fixed spinning wheel position

* added horizontal scrolling to steps tree

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

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

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

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

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

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

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

## Not covered in this PR
- Need to actually implement support for defining the provider and connection IDs for a notebook. this will be important to support New Notebook from a big data connection in Object Explorer
- Need to add APIs for adding cells, to support 
- Need to implement the metadata for getting full notebook contents. I've only implemented to key APIs needed to make this all work.
2018-12-03 18:50:44 -08:00
Raj
cb162b16f2 No kernel is shown when open a new notebook from command palette (#3374)
Fixes #3271. Ensure a provider is defined when opening through command palette
2018-12-03 18:12:25 -08:00
Karl Burtram
86e54ce145 Bump Azure Data Studio to 1.3.7 2018-12-03 17:43:12 -08:00
Anthony Dresser
efd809971f fix row select (#3390) 2018-12-03 14:10:50 -08:00
Karl Burtram
38ae14cc4d Use UTF8 for Azure token cache (#3391)
* Switch token cache encryption encoding to UTF8

* Try to parse as binary in fallback

* Code review feedback
2018-12-03 14:09:42 -08:00
Chris LaFreniere
c7e33a90fe Notebook toolbar extensibility (#3362)
* Notebook Toolbar Contribution

* Address CR comments, ensure CSS can be passed in for contributed items
2018-12-03 11:15:14 -08:00
Yurong He
5add835750 Fixed issue: Notebook: Can't use PySpark3 even though having big data cluster connected #3363 (#3380) 2018-12-03 10:53:04 -08:00
Karl Burtram
734c614cba Update installer to regkeys for per-user install (#3376) 2018-11-30 16:56:19 -08:00
Karl Burtram
f6b347fa62 Bump Azure Data Studio to 1.3.6 2018-11-30 16:51:27 -08:00
Alan Ren
08d2f3125e Fix for issue 3133 (#3375)
* Fix for issue 3133

* fix test error
2018-11-30 15:53:36 -08:00
Matt Irvine
385c48dcad Wait for account provider registration when using account service (#3221) 2018-11-30 11:52:01 -08:00
Yurong He
0926057bfe Fixed #3294 removed dead code for "attach to" and remove disable to make it respond theme change (#3349)
* Fixed #3294

* Fixed #3294
2018-11-29 19:28:08 -08:00
Karl Burtram
6912e3893e Bump Azure Data Studio to 1.3.5 2018-11-29 17:12:32 -08:00
Karl Burtram
d3052657df Bump SQL Tools to 1.5.0-alpha.60 2018-11-29 17:12:05 -08:00
Karl Burtram
a5ca4d8edf Add ALTER and PROC to the colorization list (#3353) 2018-11-29 17:04:45 -08:00
Anthony Dresser
afb1ebebd5 Result Streaming (#3319)
* handle releasing data when the grid is unrendered

* update sqlops

* add complete to sqlops

* update protocol

* update protocol

* formatting

* update sqlops.d.ts

* stash

* better handling of results streaming

* formatting

* improvments to result streaming

* bump slickgrid and address a performance bottleneck

* remove unnecessary code

* formatting

* update locks

* optimize large values in the grid

* formatting

* formatting

* update yarn

* bump packages

* yarn

* bump

* yarn lock

* locking

* yarn

* fix duplicate result sets

* fix event stream versions

* yarn
2018-11-29 15:16:53 -08:00
Matt Irvine
a04a9eb5ad Hide connection account picker when there are no auth type options (#3350) 2018-11-29 14:54:46 -08:00
Cory Rivera
027badd21f Fix capitalization in dataTierApplicationWizard imports. (#3351) 2018-11-29 14:32:45 -08:00
Raj
1affc760e6 Notebook save functionality through button (#3340)
* 3268: Notebook save using button

* 3268: Misc change

* Handle promise while saving notebook

* Async await functions to run action
2018-11-29 14:10:16 -08:00
Yurong He
3ca72b7398 Fixed Spark UI links within Notebooks are not working notebooks sql2019Preview (#3344)
#3277
2018-11-29 12:52:18 -08:00
Yurong He
702dbddd78 Fixed some issues for Markdown and CSS (#3336)
* Fixed few markdown cell issues
* Change the boolean value from 1 to true
2018-11-29 12:50:53 -08:00
Aditya Bist
8fbecc0227 Fixed agent step update (#3308)
* fixed agent step update

* enabled reordering of steps in edit job
2018-11-29 10:48:12 -08:00
Alan Ren
421271acfa hide the profiler commands meant for internal use (#3339) 2018-11-28 15:03:53 -08:00
Raj
98af76b3ac Build error (#3335) 2018-11-28 10:45:21 -08:00
Raj
3952fdbe2d 3240: Empty cells show thick border (#3242)
* 3240: Empty cells show thick border

* 3240: Refactoring code and markdown css selection +output UI improvement
2018-11-28 09:45:34 -08:00
Yurong He
bc13beaa85 Revert "Hide run button in markdown editor and pull toggleMoreAction to a seperate class (#3321)" (#3328)
This reverts commit 9ea8baca05.
2018-11-27 23:43:16 -08:00
Karl Burtram
59b2e706ca Bump Import extension to 0.4.2 2018-11-27 17:59:34 -08:00
Karl Burtram
8bf835c531 Update SQL Tools Service to 1.5.0-alpha.59 2018-11-27 16:37:46 -08:00
Matt Irvine
087ed7c132 Make resource parameter optional for getSecurityToken API (#3322) 2018-11-27 16:12:30 -08:00
kisantia
4c075df327 DacFx import/export wizard (#3162)
Basic wizard to import/export bacpacs and deploy/extract dacpacs
2018-11-27 16:10:17 -08:00
Yurong He
9ea8baca05 Hide run button in markdown editor and pull toggleMoreAction to a seperate class (#3321) 2018-11-27 15:32:20 -08:00
Karl Burtram
9b6784720e Add EXCEPT,INTERSECT and DATALENGTH to color syntax (#3320) 2018-11-27 13:52:07 -08:00
Karl Burtram
3761e1dd60 Port event-stream changes from vscode (#3317) 2018-11-27 12:55:44 -08:00
Yurong He
b3eb809550 Hide ToggleMoreAction for inactive cell (#3235)
* Hide ToggleMoreAction for inactive cell

* Revert wrong merge

* Undo bad merge

* Enable markdown to hide ToggleMoreAction

* Resolve PR comments

* Fixed the name

* Change name

* Fix toggleMoreActions by passing in CellContext instead of just nb model

* Fixed the warning by removing notificationService from the caller
2018-11-27 12:54:27 -08:00
Matt Irvine
cb72865dcc Enable Azure Active Directory MFA authentication (#3125) 2018-11-27 11:13:47 -08:00
Anthony Dresser
d646b4729b Revert "Result Streaming (#3124)" (#3312)
This reverts commit 8925d44807.
2018-11-27 10:42:41 -08:00
Karl Burtram
a2dd903d0d Bump import, profiler, and agent extensions 2018-11-27 07:25:31 -08:00
Karl Burtram
28ed378ee7 Bump Azure Data Studio to 1.3.4 2018-11-27 05:53:43 -08:00
162 changed files with 7478 additions and 2638 deletions

View File

@@ -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"

View File

@@ -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",

View File

@@ -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
}; };
} }
} }

View File

@@ -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;
} }

View File

@@ -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();

View File

@@ -116,6 +116,10 @@ export class AlertDialog extends AgentDialog<AlertData> {
private static readonly DelayMinutesTextBoxLabel: string = localize('alertDialog.DelayMinutes', 'Delay Minutes'); private static readonly 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) {

View File

@@ -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 = [];
} }

View File

@@ -67,6 +67,9 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
private readonly QuitJobReportingSuccess: string = localize('jobStepDialog.quitJobSuccess', 'Quit the job reporting success'); private readonly 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() {

View File

@@ -43,6 +43,10 @@ export class OperatorDialog extends AgentDialog<OperatorData> {
private static readonly AlertEmailColumnLabel: string = localize('createOperator.AlertEmailColumnLabel', 'E-mail'); private static readonly 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) {

View File

@@ -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) {

View File

@@ -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();
}); });
} }

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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'
} }
} }

View File

@@ -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);

View File

@@ -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;

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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);

View File

@@ -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());

View File

@@ -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());

View File

@@ -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());

View File

@@ -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"
} }
] ]
} }

View File

@@ -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));
}
} }

View 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;
}
}

View 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;
}

View File

@@ -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;
}
} }

View File

@@ -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;
}

View File

@@ -0,0 +1,253 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as sqlops from 'sqlops';
import { SelectOperationPage } from './pages/selectOperationpage';
import { DeployConfigPage } from './pages/deployConfigPage';
import { DacFxSummaryPage } from './pages/dacFxSummaryPage';
import { ExportConfigPage } from './pages/exportConfigPage';
import { ExtractConfigPage } from './pages/extractConfigPage';
import { ImportConfigPage } from './pages/importConfigPage';
import { DacFxDataModel } from './api/models';
import { BasePage } from './api/basePage';
const localize = nls.loadMessageBundle();
class Page {
wizardPage: sqlops.window.modelviewdialog.WizardPage;
dacFxPage: BasePage;
constructor(wizardPage: sqlops.window.modelviewdialog.WizardPage) {
this.wizardPage = wizardPage;
}
}
export enum Operation {
deploy,
extract,
import,
export
}
export class DataTierApplicationWizard {
public wizard: sqlops.window.modelviewdialog.Wizard;
private connection: sqlops.connection.Connection;
private model: DacFxDataModel;
public pages: Map<string, Page> = new Map<string, Page>();
public selectedOperation: Operation;
constructor() {
}
public async start(p: any, ...args: any[]) {
this.model = <DacFxDataModel>{};
let profile = p ? <sqlops.IConnectionProfile>p.connectionProfile : undefined;
if (profile) {
this.model.serverId = profile.id;
this.model.database = profile.databaseName;
}
this.connection = await sqlops.connection.getCurrentConnection();
if (!this.connection) {
this.connection = await sqlops.connection.openConnectionDialog();
}
this.wizard = sqlops.window.modelviewdialog.createWizard('Data-tier Application Wizard');
let selectOperationWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.selectOperationPageName', 'Select an Operation'));
let deployConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.deployConfigPageName', 'Select Deploy Dacpac Settings'));
let summaryWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.summaryPageName', 'Summary'));
let extractConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.extractConfigPageName', 'Select Extract Dacpac Settings'));
let importConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.importConfigPageName', 'Select Import Bacpac Settings'));
let exportConfigWizardPage = sqlops.window.modelviewdialog.createWizardPage(localize('dacFx.exportConfigPageName', 'Select Export Bacpac Settings'));
this.pages.set('selectOperation', new Page(selectOperationWizardPage));
this.pages.set('deployConfig', new Page(deployConfigWizardPage));
this.pages.set('extractConfig', new Page(extractConfigWizardPage));
this.pages.set('importConfig', new Page(importConfigWizardPage));
this.pages.set('exportConfig', new Page(exportConfigWizardPage));
this.pages.set('summary', new Page(summaryWizardPage));
selectOperationWizardPage.registerContent(async (view) => {
let selectOperationDacFxPage = new SelectOperationPage(this, selectOperationWizardPage, this.model, view);
this.pages.get('selectOperation').dacFxPage = selectOperationDacFxPage;
await selectOperationDacFxPage.start().then(() => {
selectOperationDacFxPage.setupNavigationValidator();
selectOperationDacFxPage.onPageEnter();
});
});
deployConfigWizardPage.registerContent(async (view) => {
let deployConfigDacFxPage = new DeployConfigPage(this, deployConfigWizardPage, this.model, view);
this.pages.get('deployConfig').dacFxPage = deployConfigDacFxPage;
await deployConfigDacFxPage.start();
});
extractConfigWizardPage.registerContent(async (view) => {
let extractConfigDacFxPage = new ExtractConfigPage(this, extractConfigWizardPage, this.model, view);
this.pages.get('extractConfig').dacFxPage = extractConfigDacFxPage;
await extractConfigDacFxPage.start();
});
importConfigWizardPage.registerContent(async (view) => {
let importConfigDacFxPage = new ImportConfigPage(this, importConfigWizardPage, this.model, view);
this.pages.get('importConfig').dacFxPage = importConfigDacFxPage;
await importConfigDacFxPage.start();
});
exportConfigWizardPage.registerContent(async (view) => {
let exportConfigDacFxPage = new ExportConfigPage(this, exportConfigWizardPage, this.model, view);
this.pages.get('exportConfig').dacFxPage = exportConfigDacFxPage;
await exportConfigDacFxPage.start();
});
summaryWizardPage.registerContent(async (view) => {
let summaryDacFxPage = new DacFxSummaryPage(this, summaryWizardPage, this.model, view);
this.pages.get('summary').dacFxPage = summaryDacFxPage;
await summaryDacFxPage.start();
});
this.wizard.onPageChanged(async (event) => {
let idx = event.newPage;
let page: Page;
if (idx === 1) {
switch (this.selectedOperation) {
case Operation.deploy: {
page = this.pages.get('deployConfig');
break;
}
case Operation.extract: {
page = this.pages.get('extractConfig');
break;
}
case Operation.import: {
page = this.pages.get('importConfig');
break;
}
case Operation.export: {
page = this.pages.get('exportConfig');
break;
}
}
} else if (idx === 2) {
page = this.pages.get('summary');
}
if (page !== undefined) {
page.dacFxPage.setupNavigationValidator();
page.dacFxPage.onPageEnter();
}
});
this.wizard.pages = [selectOperationWizardPage, deployConfigWizardPage, summaryWizardPage];
this.wizard.generateScriptButton.hidden = true;
this.wizard.doneButton.onClick(async () => await this.executeOperation());
this.wizard.open();
}
public registerNavigationValidator(validator: (pageChangeInfo: sqlops.window.modelviewdialog.WizardPageChangeInfo) => boolean) {
this.wizard.registerNavigationValidator(validator);
}
public setDoneButton(operation: Operation): void {
switch (operation) {
case Operation.deploy: {
this.wizard.doneButton.label = localize('dacFx.deployButton', 'Deploy');
this.selectedOperation = Operation.deploy;
break;
}
case Operation.extract: {
this.wizard.doneButton.label = localize('dacFx.extractButton', 'Extract');
this.selectedOperation = Operation.extract;
break;
}
case Operation.import: {
this.wizard.doneButton.label = localize('dacFx.importButton', 'Import');
this.selectedOperation = Operation.import;
break;
}
case Operation.export: {
this.wizard.doneButton.label = localize('dacFx.exportButton', 'Export');
this.selectedOperation = Operation.export;
break;
}
}
}
private async executeOperation() {
switch (this.selectedOperation) {
case Operation.deploy: {
await this.deploy();
break;
}
case Operation.extract: {
await this.extract();
break;
}
case Operation.import: {
await this.import();
break;
}
case Operation.export: {
await this.export();
break;
}
}
}
private async deploy() {
let service = await DataTierApplicationWizard.getService();
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
let result = await service.deployDacpac(this.model.filePath, this.model.database, this.model.upgradeExisting, ownerUri, sqlops.TaskExecutionMode.execute);
if (!result || !result.success) {
vscode.window.showErrorMessage(
localize('alertData.deployErrorMessage', "Deploy failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
}
}
private async extract() {
let service = await DataTierApplicationWizard.getService();
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
let result = await service.extractDacpac(this.model.database, this.model.filePath, this.model.database, this.model.version, ownerUri, sqlops.TaskExecutionMode.execute);
if (!result || !result.success) {
vscode.window.showErrorMessage(
localize('alertData.extractErrorMessage', "Extract failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
}
}
private async export() {
let service = await DataTierApplicationWizard.getService();
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
let result = await service.exportBacpac(this.model.database, this.model.filePath, ownerUri, sqlops.TaskExecutionMode.execute);
if (!result || !result.success) {
vscode.window.showErrorMessage(
localize('alertData.exportErrorMessage', "Export failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
}
}
private async import() {
let service = await DataTierApplicationWizard.getService();
let ownerUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId);
let result = await service.importBacpac(this.model.filePath, this.model.database, ownerUri, sqlops.TaskExecutionMode.execute);
if (!result || !result.success) {
vscode.window.showErrorMessage(
localize('alertData.importErrorMessage', "Import failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
}
}
public static async getService(): Promise<sqlops.DacFxServicesProvider> {
let currentConnection = await sqlops.connection.getCurrentConnection();
let service = sqlops.dataprotocol.getProvider<sqlops.DacFxServicesProvider>(currentConnection.providerName, sqlops.DataProviderType.DacFxServicesProvider);
return service;
}
}

View 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
});
}
}

View File

@@ -0,0 +1,190 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as nls from 'vscode-nls';
import * as vscode from 'vscode';
import * as path from 'path';
import * as os from 'os';
import { DacFxDataModel } from '../api/models';
import { DataTierApplicationWizard } from '../dataTierApplicationWizard';
import { DacFxConfigPage } from '../api/dacFxConfigPage';
const localize = nls.loadMessageBundle();
export class DeployConfigPage extends DacFxConfigPage {
protected readonly wizardPage: sqlops.window.modelviewdialog.WizardPage;
protected readonly instance: DataTierApplicationWizard;
protected readonly model: DacFxDataModel;
protected readonly view: sqlops.ModelView;
private databaseDropdownComponent: sqlops.FormComponent;
private databaseComponent: sqlops.FormComponent;
private formBuilder: sqlops.FormBuilder;
private form: sqlops.FormContainer;
public constructor(instance: DataTierApplicationWizard, wizardPage: sqlops.window.modelviewdialog.WizardPage, model: DacFxDataModel, view: sqlops.ModelView) {
super(instance, wizardPage, model, view);
this.fileExtension = '.bacpac';
}
async start(): Promise<boolean> {
let serverComponent = await this.createServerDropdown(true);
let fileBrowserComponent = await this.createFileBrowser();
this.databaseComponent = await this.createDatabaseTextBox();
this.databaseComponent.title = localize('dacFx.databaseNameTextBox', 'Database Name');
this.databaseDropdownComponent = await this.createDeployDatabaseDropdown();
this.databaseDropdownComponent.title = localize('dacFx.databaseNameDropdown', 'Database Name');
let radioButtons = await this.createRadiobuttons();
this.formBuilder = this.view.modelBuilder.formContainer()
.withFormItems(
[
fileBrowserComponent,
serverComponent,
radioButtons,
this.databaseDropdownComponent
], {
horizontal: true,
componentWidth: 400
});
this.form = this.formBuilder.component();
await this.view.initializeModel(this.form);
return true;
}
async onPageEnter(): Promise<boolean> {
let r1 = await this.populateServerDropdown();
let r2 = await this.populateDeployDatabaseDropdown();
return r1 && r2;
}
private async createFileBrowser(): Promise<sqlops.FormComponent> {
this.createFileBrowserParts();
this.fileButton.onDidClick(async (click) => {
let fileUris = await vscode.window.showOpenDialog(
{
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: vscode.Uri.file(os.homedir()),
openLabel: localize('dacFxDeploy.openFile', 'Open'),
filters: {
'dacpac Files': ['dacpac'],
}
}
);
if (!fileUris || fileUris.length === 0) {
return;
}
let fileUri = fileUris[0];
this.fileTextBox.value = fileUri.fsPath;
this.model.filePath = fileUri.fsPath;
});
this.fileTextBox.onTextChanged(async () => {
this.model.filePath = this.fileTextBox.value;
this.databaseTextBox.value = this.generateDatabaseName(this.model.filePath);
if (!this.model.upgradeExisting) {
this.model.database = this.databaseTextBox.value;
}
});
return {
component: this.fileTextBox,
title: localize('dacFxDeploy.fileTextboxTitle', 'File Location'),
actions: [this.fileButton]
};
}
private async createRadiobuttons(): Promise<sqlops.FormComponent> {
let upgradeRadioButton = this.view.modelBuilder.radioButton()
.withProperties({
name: 'updateExisting',
label: localize('dacFx.upgradeRadioButtonLabel', 'Upgrade Existing Database'),
}).component();
let newRadioButton = this.view.modelBuilder.radioButton()
.withProperties({
name: 'updateExisting',
label: localize('dacFx.newRadioButtonLabel', 'New Database'),
}).component();
upgradeRadioButton.onDidClick(() => {
this.model.upgradeExisting = true;
this.formBuilder.removeFormItem(this.databaseComponent);
this.formBuilder.addFormItem(this.databaseDropdownComponent, { horizontal: true, componentWidth: 400 });
this.model.database = (<sqlops.CategoryValue>this.databaseDropdown.value).name;
});
newRadioButton.onDidClick(() => {
this.model.upgradeExisting = false;
this.formBuilder.removeFormItem(this.databaseDropdownComponent);
this.formBuilder.addFormItem(this.databaseComponent, { horizontal: true, componentWidth: 400 });
this.model.database = this.databaseTextBox.value;
});
// Initialize with upgrade existing true
upgradeRadioButton.checked = true;
this.model.upgradeExisting = true;
let flexRadioButtonsModel = this.view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'row',
}).withItems([
upgradeRadioButton, newRadioButton]
).component();
return {
component: flexRadioButtonsModel,
title: localize('dacFx.targetDatabaseRadioButtonsTitle', 'Target Database')
};
}
protected async createDeployDatabaseDropdown(): Promise<sqlops.FormComponent> {
this.databaseDropdown = this.view.modelBuilder.dropDown().withProperties({
required: true
}).component();
// Handle database changes
this.databaseDropdown.onValueChanged(async () => {
this.model.database = (<sqlops.CategoryValue>this.databaseDropdown.value).name;
});
this.databaseLoader = this.view.modelBuilder.loadingComponent().withItem(this.databaseDropdown).component();
return {
component: this.databaseLoader,
title: localize('dacFx.targetDatabaseDropdownTitle', 'Database Name')
};
}
protected async populateDeployDatabaseDropdown(): Promise<boolean> {
this.databaseLoader.loading = true;
this.databaseDropdown.updateProperties({ values: [] });
if (!this.model.server) {
this.databaseLoader.loading = false;
return false;
}
let values = await this.getDatabaseValues();
if (this.model.database === undefined) {
this.model.database = values[0].name;
}
this.databaseDropdown.updateProperties({
values: values
});
this.databaseLoader.loading = false;
return true;
}
private generateDatabaseName(filePath: string): string {
let result = path.parse(filePath);
return result.name;
}
}

View 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]
};
}
}

View 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)'),
};
}
}

View File

@@ -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;

View File

@@ -0,0 +1,101 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * 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;
}
}

View 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;
});
}
}

View File

@@ -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,

View File

@@ -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",

View File

@@ -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 > ------------------------------------

View File

@@ -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
}; };

View File

@@ -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()
}; };

View File

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

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
let counter = 0;
export function activate(extensionContext: vscode.ExtensionContext) {
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.new', () => {
let title = `Untitled-${counter++}`;
let untitledUri = vscode.Uri.parse(`untitled:${title}`);
sqlops.nb.showNotebookDocument(untitledUri).then(success => {
}, (err: Error) => {
vscode.window.showErrorMessage(err.message);
});
}));
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.open', () => {
openNotebook();
}));
}
async function openNotebook(): Promise<void> {
try {
let filter = {};
// TODO support querying valid notebook file types
filter[localize('notebookFiles', 'Notebooks')] = ['ipynb'];
let file = await vscode.window.showOpenDialog({
filters: filter
});
if (file) {
let doc = await vscode.workspace.openTextDocument(file[0]);
vscode.window.showTextDocument(doc);
}
} catch (err) {
vscode.window.showErrorMessage(err);
}
}
// this method is called when your extension is deactivated
export function deactivate() {
}

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
"name": "profiler", "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

View File

@@ -1,6 +1,6 @@
{ {
"name": "azuredatastudio", "name": "azuredatastudio",
"version": "1.3.3", "version": "1.3.7",
"distro": "8c3e97e3425cc9814496472ab73e076de2ba99ee", "distro": "8c3e97e3425cc9814496472ab73e076de2ba99ee",
"author": { "author": {
"name": "Microsoft Corporation" "name": "Microsoft Corporation"
@@ -90,6 +90,7 @@
"@types/mocha": "2.2.39", "@types/mocha": "2.2.39",
"@types/sanitize-html": "^1.18.2", "@types/sanitize-html": "^1.18.2",
"@types/semver": "5.3.30", "@types/semver": "5.3.30",
"@types/should": "^13.0.0",
"@types/sinon": "1.16.34", "@types/sinon": "1.16.34",
"@types/winreg": "^1.2.30", "@types/winreg": "^1.2.30",
"asar": "^0.14.0", "asar": "^0.14.0",
@@ -102,12 +103,12 @@
"documentdb": "^1.5.1", "documentdb": "^1.5.1",
"electron-mksnapshot": "~1.7.0", "electron-mksnapshot": "~1.7.0",
"eslint": "^3.4.0", "eslint": "^3.4.0",
"event-stream": "^3.3.4", "event-stream": "3.3.4",
"express": "^4.13.1", "express": "^4.13.1",
"glob": "^5.0.13", "glob": "^5.0.13",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"gulp-atom-electron": "^1.16.1", "gulp-atom-electron": "^1.19.2",
"gulp-azure-storage": "^0.7.0", "gulp-azure-storage": "^0.8.2",
"gulp-bom": "^1.0.0", "gulp-bom": "^1.0.0",
"gulp-buffer": "0.0.2", "gulp-buffer": "0.0.2",
"gulp-cli": "^2.0.1", "gulp-cli": "^2.0.1",
@@ -120,7 +121,7 @@
"gulp-json-editor": "^2.2.1", "gulp-json-editor": "^2.2.1",
"gulp-mocha": "^2.1.3", "gulp-mocha": "^2.1.3",
"gulp-plumber": "^1.2.0", "gulp-plumber": "^1.2.0",
"gulp-remote-src": "^0.4.0", "gulp-remote-src": "^0.4.4",
"gulp-rename": "^1.2.0", "gulp-rename": "^1.2.0",
"gulp-replace": "^0.5.4", "gulp-replace": "^0.5.4",
"gulp-shell": "^0.5.2", "gulp-shell": "^0.5.2",
@@ -129,7 +130,7 @@
"gulp-tslint": "^8.1.2", "gulp-tslint": "^8.1.2",
"gulp-uglify": "^3.0.0", "gulp-uglify": "^3.0.0",
"gulp-util": "^3.0.6", "gulp-util": "^3.0.6",
"gulp-vinyl-zip": "^1.2.2", "gulp-vinyl-zip": "^2.1.2",
"husky": "^0.13.1", "husky": "^0.13.1",
"innosetup-compiler": "^5.5.60", "innosetup-compiler": "^5.5.60",
"is": "^3.1.0", "is": "^3.1.0",
@@ -148,8 +149,10 @@
"queue": "3.0.6", "queue": "3.0.6",
"remap-istanbul": "^0.6.4", "remap-istanbul": "^0.6.4",
"rimraf": "^2.2.8", "rimraf": "^2.2.8",
"should": "^13.2.3",
"sinon": "^1.17.2", "sinon": "^1.17.2",
"source-map": "^0.4.4", "source-map": "^0.4.4",
"temp-write": "^3.4.0",
"tslint": "^5.9.1", "tslint": "^5.9.1",
"typemoq": "^0.3.2", "typemoq": "^0.3.2",
"typescript": "2.9.2", "typescript": "2.9.2",
@@ -174,6 +177,7 @@
"windows-process-tree": "0.2.2" "windows-process-tree": "0.2.2"
}, },
"resolutions": { "resolutions": {
"rc": "1.2.8" "rc": "1.2.8",
"event-stream": "3.3.4"
} }
} }

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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)
}; };

View File

@@ -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';

View File

@@ -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;
} }

View File

@@ -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,

View File

@@ -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,

View File

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

View File

@@ -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

View File

@@ -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
}); });

View File

@@ -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;

View File

@@ -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';

View File

@@ -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;
}
} }

View File

@@ -28,11 +28,12 @@
overflow: hidden; overflow: hidden;
margin: 0px 11px; margin: 0px 11px;
} }
.connection-dialog .tabBody { .connection-dialog .tabBody {
overflow: hidden; overflow: hidden;
flex: 1 1; flex: 1 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.connection-recent, .connection-saved { .connection-recent, .connection-saved {
@@ -114,4 +115,20 @@
margin: 5px 0px; margin: 5px 0px;
padding: 5px 15px; padding: 5px 15px;
font-weight: 600; font-weight: 600;
} }
.hide-azure-accounts .azure-account-row {
display: none;
}
.hide-username-password .username-password-row {
display: none;
}
.hide-refresh-link .azure-account-row.refresh-credentials-link {
display: none;
}
.hide-azure-tenants .azure-tenant-row {
display: none;
}

View File

@@ -17,6 +17,9 @@ import { AlertsViewComponent } from 'sql/parts/jobManagement/views/alertsView.co
import { OperatorsViewComponent } from 'sql/parts/jobManagement/views/operatorsView.component'; import { 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}",

View File

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

View File

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

View File

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

View File

@@ -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>

View File

@@ -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);
}
} }
} }

View File

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

View File

@@ -5,6 +5,7 @@
import * as DOM from 'vs/base/browser/dom'; import * 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);

View File

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

View File

@@ -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);
} }

View File

@@ -9,6 +9,6 @@
</div> </div>
<div #editor class="editor" style="flex: 1 1 auto; overflow: hidden;"> <div #editor class="editor" style="flex: 1 1 auto; overflow: hidden;">
</div> </div>
<div #moreactions class="toolbar" style="flex: 0 0 auto; display: flex; flex-flow:column; width: 20px; min-height: 20px; max-height: 20px; padding-top: 10px; orientation: portrait"> <div #moreactions class="toolbar" style="flex: 0 0 auto; display: flex; flex-flow:column; width: 20px; min-height: 20px; max-height: 20px; padding-top: 0px; orientation: portrait">
</div> </div>
</div> </div>

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import 'vs/css!./code'; import 'vs/css!./code';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter } from '@angular/core'; import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { AngularDisposable } from 'sql/base/common/lifecycle'; import { AngularDisposable } from 'sql/base/common/lifecycle';
@@ -41,11 +41,12 @@ export const CODE_SELECTOR: string = 'code-component';
selector: CODE_SELECTOR, selector: CODE_SELECTOR,
templateUrl: decodeURI(require.toUrl('./code.component.html')) templateUrl: decodeURI(require.toUrl('./code.component.html'))
}) })
export class CodeComponent extends AngularDisposable implements OnInit { export class CodeComponent extends AngularDisposable implements OnInit, OnChanges {
@ViewChild('toolbar', { read: ElementRef }) private toolbarElement: ElementRef; @ViewChild('toolbar', { read: ElementRef }) private toolbarElement: ElementRef;
@ViewChild('moreactions', { read: ElementRef }) private moreactionsElement: ElementRef; @ViewChild('moreactions', { read: ElementRef }) private moreActionsElementRef: ElementRef;
@ViewChild('editor', { read: ElementRef }) private codeElement: ElementRef; @ViewChild('editor', { read: ElementRef }) private codeElement: ElementRef;
@Input() cellModel: ICellModel; @Input() cellModel: ICellModel;
@Input() hideVerticalToolbar: boolean = false;
@Output() public onContentChanged = new EventEmitter<void>(); @Output() public onContentChanged = new EventEmitter<void>();
@@ -53,6 +54,10 @@ export class CodeComponent extends AngularDisposable implements OnInit {
this._model = value; this._model = value;
} }
@Input() set activeCellId(value: string) {
this._activeCellId = value;
}
protected _actionBar: Taskbar; protected _actionBar: Taskbar;
protected _moreActions: ActionBar; protected _moreActions: ActionBar;
private readonly _minimumHeight = 30; private readonly _minimumHeight = 30;
@@ -61,6 +66,8 @@ export class CodeComponent extends AngularDisposable implements OnInit {
private _editorModel: ITextModel; private _editorModel: ITextModel;
private _uri: string; private _uri: string;
private _model: NotebookModel; private _model: NotebookModel;
private _actions: Action[] = [];
private _activeCellId: string;
constructor( constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface, @Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
@@ -74,17 +81,38 @@ export class CodeComponent extends AngularDisposable implements OnInit {
@Inject(INotificationService) private notificationService: INotificationService, @Inject(INotificationService) private notificationService: INotificationService,
) { ) {
super(); super();
this._actions.push(
this._instantiationService.createInstance(AddCellAction, 'codeBefore', localize('codeBefore', 'Insert Code before'), CellTypes.Code, false),
this._instantiationService.createInstance(AddCellAction, 'codeAfter', localize('codeAfter', 'Insert Code after'), CellTypes.Code, true),
this._instantiationService.createInstance(AddCellAction, 'markdownBefore', localize('markdownBefore', 'Insert Markdown before'), CellTypes.Markdown, false),
this._instantiationService.createInstance(AddCellAction, 'markdownAfter', localize('markdownAfter', 'Insert Markdown after'), CellTypes.Markdown, true),
this._instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', 'Delete'))
);
} }
ngOnInit() { ngOnInit() {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this)); this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme()); this.updateTheme(this.themeService.getColorTheme());
this.initActionBar(); if (!this.hideVerticalToolbar) {
this.initActionBar();
}
} }
ngOnChanges() { ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
this.updateLanguageMode(); this.updateLanguageMode();
this.updateModel(); this.updateModel();
for (let propName in changes) {
if (propName === 'activeCellId') {
let changedProp = changes[propName];
if (this.cellModel.id === changedProp.currentValue) {
this.toggleMoreActions(true);
}
else {
this.toggleMoreActions(false);
}
break;
}
}
} }
ngAfterContentInit(): void { ngAfterContentInit(): void {
@@ -98,6 +126,10 @@ export class CodeComponent extends AngularDisposable implements OnInit {
return this._model; return this._model;
} }
get activeCellId(): string {
return this._activeCellId;
}
private createEditor(): void { private createEditor(): void {
let instantiationService = this._instantiationService.createChild(new ServiceCollection([IProgressService, new SimpleProgressService()])); let instantiationService = this._instantiationService.createChild(new ServiceCollection([IProgressService, new SimpleProgressService()]));
this._editor = instantiationService.createInstance(QueryTextEditor); this._editor = instantiationService.createInstance(QueryTextEditor);
@@ -139,19 +171,22 @@ export class CodeComponent extends AngularDisposable implements OnInit {
this._actionBar.setContent([ this._actionBar.setContent([
{ action: runCellAction } { action: runCellAction }
]); ]);
}
let moreActionsElement = <HTMLElement>this.moreactionsElement.nativeElement; private toggleMoreActions(showIcon: boolean) {
this._moreActions = new ActionBar(moreActionsElement, { orientation: ActionsOrientation.VERTICAL }); let context = new CellContext(this.model, this.cellModel);
this._moreActions.context = { target: moreActionsElement }; let moreActionsElement = <HTMLElement>this.moreActionsElementRef.nativeElement;
if (showIcon) {
let actions: Action[] = []; if (moreActionsElement.childNodes.length > 0) {
actions.push(this._instantiationService.createInstance(AddCellAction, 'codeBefore', localize('codeBefore', 'Insert Code before'), CellTypes.Code, false)); moreActionsElement.removeChild(moreActionsElement.childNodes[0]);
actions.push(this._instantiationService.createInstance(AddCellAction, 'codeAfter', localize('codeAfter', 'Insert Code after'), CellTypes.Code, true)); }
actions.push(this._instantiationService.createInstance(AddCellAction, 'markdownBefore', localize('markdownBefore', 'Insert Markdown before'), CellTypes.Markdown, false)); this._moreActions = new ActionBar(moreActionsElement, { orientation: ActionsOrientation.VERTICAL });
actions.push(this._instantiationService.createInstance(AddCellAction, 'markdownAfter', localize('markdownAfter', 'Insert Markdown after'), CellTypes.Markdown, true)); this._moreActions.context = { target: moreActionsElement };
actions.push(this._instantiationService.createInstance(DeleteCellAction, 'delete', localize('delete', 'Delete'))); this._moreActions.push(this._instantiationService.createInstance(ToggleMoreWidgetAction, this._actions, context), { icon: showIcon, label: false });
}
this._moreActions.push(this._instantiationService.createInstance(ToggleMoreWidgetAction, actions, context), { icon: true, label: false }); else if (moreActionsElement.childNodes.length > 0) {
moreActionsElement.removeChild(moreActionsElement.childNodes[0]);
}
} }
private createUri(): URI { private createUri(): URI {
@@ -180,8 +215,8 @@ export class CodeComponent extends AngularDisposable implements OnInit {
let toolbarEl = <HTMLElement>this.toolbarElement.nativeElement; let toolbarEl = <HTMLElement>this.toolbarElement.nativeElement;
toolbarEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString(); toolbarEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
let moreactionsEl = <HTMLElement>this.moreactionsElement.nativeElement; let moreActionsEl = <HTMLElement>this.moreActionsElementRef.nativeElement;
moreactionsEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString(); moreActionsEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
} }
} }

View File

@@ -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 {

View File

@@ -173,7 +173,7 @@ export class DeleteCellAction extends CellActionBase {
runCellAction(context: CellContext): Promise<void> { runCellAction(context: CellContext): Promise<void> {
try { try {
context.model.deleteCell(context.cell); context.model.deleteCell(context.cell);
} catch (error) { } catch (error) {
let message = getErrorMessage(error); let message = getErrorMessage(error);

View File

@@ -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>

View File

@@ -2,15 +2,10 @@
* Copyright (c) Microsoft Corporation. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import 'vs/css!./codeCell';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, SimpleChange, OnChanges } from '@angular/core';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { CellView } from 'sql/parts/notebook/cellViews/interfaces'; import { CellView } from 'sql/parts/notebook/cellViews/interfaces';
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import * as themeColors from 'vs/workbench/common/theme';
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel'; import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
@@ -21,24 +16,27 @@ export const CODE_SELECTOR: string = 'code-cell-component';
selector: CODE_SELECTOR, selector: CODE_SELECTOR,
templateUrl: decodeURI(require.toUrl('./codeCell.component.html')) templateUrl: decodeURI(require.toUrl('./codeCell.component.html'))
}) })
export class CodeCellComponent extends CellView implements OnInit {
export class CodeCellComponent extends CellView implements OnInit, OnChanges {
@ViewChild('codeCellOutput', { read: ElementRef }) private outputPreview: ElementRef; @ViewChild('codeCellOutput', { read: ElementRef }) private outputPreview: ElementRef;
private _model: NotebookModel;
@Input() cellModel: ICellModel; @Input() cellModel: ICellModel;
@Input() set model(value: NotebookModel) { @Input() set model(value: NotebookModel) {
this._model = value; this._model = value;
} }
@Input() set activeCellId(value: string) {
this._activeCellId = value;
}
private _model: NotebookModel;
private _activeCellId: string;
constructor( constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService
) { ) {
super(); super();
} }
ngOnInit() { ngOnInit() {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
if (this.cellModel) { if (this.cellModel) {
this.cellModel.onOutputsChanged(() => { this.cellModel.onOutputsChanged(() => {
this._changeRef.detectChanges(); this._changeRef.detectChanges();
@@ -46,18 +44,26 @@ export class CodeCellComponent extends CellView implements OnInit {
} }
} }
// Todo: implement layout ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
public layout() { for (let propName in changes) {
if (propName === 'activeCellId') {
} let changedProp = changes[propName];
this._activeCellId = changedProp.currentValue;
private updateTheme(theme: IColorTheme): void { break;
let outputElement = <HTMLElement>this.outputPreview.nativeElement; }
outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString(); }
} }
get model(): NotebookModel { get model(): NotebookModel {
return this._model; return this._model;
} }
get activeCellId(): string {
return this._activeCellId;
}
// Todo: implement layout
public layout() {
}
} }

View File

@@ -5,7 +5,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
--> -->
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column"> <div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div style="flex: 0 0 auto;"> <div #outputarea class="notebook-output" style="flex: 0 0 auto;">
<output-component *ngFor="let output of cellModel.outputs" [cellOutput]="output" [trustedMode] = "cellModel.trustedMode" > <output-component *ngFor="let output of cellModel.outputs" [cellOutput]="output" [trustedMode] = "cellModel.trustedMode" >
</output-component> </output-component>
</div> </div>

View File

@@ -3,9 +3,12 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import 'vs/css!./code'; import 'vs/css!./code';
import { OnInit, Component, Input, Inject, forwardRef, ChangeDetectorRef } from '@angular/core'; import 'vs/css!./outputArea';
import { OnInit, Component, Input, Inject, ElementRef, ViewChild, forwardRef, ChangeDetectorRef } from '@angular/core';
import { AngularDisposable } from 'sql/base/common/lifecycle'; import { AngularDisposable } from 'sql/base/common/lifecycle';
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
import * as themeColors from 'vs/workbench/common/theme';
import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
export const OUTPUT_AREA_SELECTOR: string = 'output-area-component'; export const OUTPUT_AREA_SELECTOR: string = 'output-area-component';
@@ -14,20 +17,30 @@ export const OUTPUT_AREA_SELECTOR: string = 'output-area-component';
templateUrl: decodeURI(require.toUrl('./outputArea.component.html')) templateUrl: decodeURI(require.toUrl('./outputArea.component.html'))
}) })
export class OutputAreaComponent extends AngularDisposable implements OnInit { export class OutputAreaComponent extends AngularDisposable implements OnInit {
@ViewChild('outputarea', { read: ElementRef }) private outputArea: ElementRef;
@Input() cellModel: ICellModel; @Input() cellModel: ICellModel;
private readonly _minimumHeight = 30; private readonly _minimumHeight = 30;
constructor( constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef
) { ) {
super(); super();
} }
ngOnInit(): void {
ngOnInit() {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
if (this.cellModel) { if (this.cellModel) {
this.cellModel.onOutputsChanged(() => { this.cellModel.onOutputsChanged(() => {
this._changeRef.detectChanges(); this._changeRef.detectChanges();
}); });
} }
} }
private updateTheme(theme: IColorTheme): void {
let outputElement = <HTMLElement>this.outputArea.nativeElement;
outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
}
} }

View File

@@ -2,13 +2,14 @@
* Copyright (c) Microsoft Corporation. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
output-area-component {
code-cell-component {
display: block; display: block;
} }
code-cell-component .notebook-output { output-area-component .notebook-output {
border-top-width: 1px; border-top-width: 1px;
border-top-style: solid; border-top-style: solid;
user-select: initial; user-select: initial;
padding: 5px 20px 0px;
box-shadow: rgba(120, 120, 120, 0.75) 0px -2px 1px -2px;
} }

View File

@@ -6,8 +6,9 @@
--> -->
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column"> <div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div class="notebook-text" style="flex: 0 0 auto;"> <div class="notebook-text" style="flex: 0 0 auto;">
<code-component *ngIf="isEditMode" [cellModel]="cellModel" (onContentChanged)="handleContentChanged()"></code-component> <code-component *ngIf="isEditMode" [cellModel]="cellModel" (onContentChanged)="handleContentChanged()" [model]="model" [activeCellId]="activeCellId" [hideVerticalToolbar]=true>
</code-component>
</div> </div>
<div #preview class="notebook-preview" style="flex: 0 0 auto;" (dblclick)="toggleEditMode()"> <div #preview style="flex: 0 0 auto;" (dblclick)="toggleEditMode()">
</div> </div>
</div> </div>

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import 'vs/css!./textCell'; import 'vs/css!./textCell';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core'; import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, OnChanges, SimpleChange } from '@angular/core';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { CellView } from 'sql/parts/notebook/cellViews/interfaces'; import { CellView } from 'sql/parts/notebook/cellViews/interfaces';
@@ -15,6 +15,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands';
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
import { ISanitizer, defaultSanitizer } from 'sql/parts/notebook/outputs/sanitizer'; import { ISanitizer, defaultSanitizer } from 'sql/parts/notebook/outputs/sanitizer';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
export const TEXT_SELECTOR: string = 'text-cell-component'; export const TEXT_SELECTOR: string = 'text-cell-component';
@@ -22,12 +23,24 @@ export const TEXT_SELECTOR: string = 'text-cell-component';
selector: TEXT_SELECTOR, selector: TEXT_SELECTOR,
templateUrl: decodeURI(require.toUrl('./textCell.component.html')) templateUrl: decodeURI(require.toUrl('./textCell.component.html'))
}) })
export class TextCellComponent extends CellView implements OnInit { export class TextCellComponent extends CellView implements OnInit, OnChanges {
@ViewChild('preview', { read: ElementRef }) private output: ElementRef; @ViewChild('preview', { read: ElementRef }) private output: ElementRef;
@Input() cellModel: ICellModel; @Input() cellModel: ICellModel;
@Input() set model(value: NotebookModel) {
this._model = value;
}
@Input() set activeCellId(value: string) {
this._activeCellId = value;
}
private _content: string; private _content: string;
private isEditMode: boolean; private isEditMode: boolean;
private _sanitizer: ISanitizer; private _sanitizer: ISanitizer;
private _previewCssApplied: boolean = false;
private _model: NotebookModel;
private _activeCellId: string;
constructor( constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface, @Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
@@ -39,10 +52,6 @@ export class TextCellComponent extends CellView implements OnInit {
this.isEditMode = false; this.isEditMode = false;
} }
ngOnChanges() {
this.updatePreview();
}
//Gets sanitizer from ISanitizer interface //Gets sanitizer from ISanitizer interface
private get sanitizer(): ISanitizer { private get sanitizer(): ISanitizer {
if (this._sanitizer) { if (this._sanitizer) {
@@ -51,13 +60,41 @@ export class TextCellComponent extends CellView implements OnInit {
return this._sanitizer = defaultSanitizer; return this._sanitizer = defaultSanitizer;
} }
get model(): NotebookModel {
return this._model;
}
get activeCellId(): string {
return this._activeCellId;
}
ngOnInit() {
this.updatePreview();
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
this.cellModel.onOutputsChanged(e => {
this.updatePreview();
});
}
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
for (let propName in changes) {
if (propName === 'activeCellId') {
let changedProp = changes[propName];
this._activeCellId = changedProp.currentValue;
this.toggleEditMode(false);
break;
}
}
}
/** /**
* Updates the preview of markdown component with latest changes * Updates the preview of markdown component with latest changes
* If content is empty and in non-edit mode, default it to 'Double-click to edit' * If content is empty and in non-edit mode, default it to 'Double-click to edit'
* Sanitizes the data to be shown in markdown cell * Sanitizes the data to be shown in markdown cell
*/ */
private updatePreview() { private updatePreview() {
if (this._content !== this.cellModel.source) { if (this._content !== this.cellModel.source || this.cellModel.source.length === 0) {
if (!this.cellModel.source && !this.isEditMode) { if (!this.cellModel.source && !this.isEditMode) {
(<HTMLElement>this.output.nativeElement).innerHTML = localize('doubleClickEdit', 'Double-click to edit'); (<HTMLElement>this.output.nativeElement).innerHTML = localize('doubleClickEdit', 'Double-click to edit');
} else { } else {
@@ -79,14 +116,6 @@ export class TextCellComponent extends CellView implements OnInit {
return content; return content;
} }
ngOnInit() {
this.updatePreview();
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
this.cellModel.onOutputsChanged(e => {
this.updatePreview();
});
}
// Todo: implement layout // Todo: implement layout
public layout() { public layout() {
@@ -98,12 +127,29 @@ export class TextCellComponent extends CellView implements OnInit {
} }
public handleContentChanged(): void { public handleContentChanged(): void {
if (!this._previewCssApplied) {
this.updatePreviewCssClass();
}
this.updatePreview(); this.updatePreview();
} }
public toggleEditMode(): void { public toggleEditMode(editMode?: boolean): void {
this.isEditMode = !this.isEditMode; this.isEditMode = editMode !== undefined? editMode : !this.isEditMode;
this.updatePreviewCssClass();
this.updatePreview(); this.updatePreview();
this._changeRef.detectChanges(); this._changeRef.detectChanges();
} }
// Updates the css class to preview 'div' based on edit mode
private updatePreviewCssClass() {
let outputElement = <HTMLElement>this.output.nativeElement;
if (this.isEditMode && this.cellModel.source) {
outputElement.className = 'notebook-preview';
this._previewCssApplied = true;
}
else {
outputElement.className = '';
this._previewCssApplied = false;
}
}
} }

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>save_inverse</title><path class="cls-1" d="M14,1a1,1,0,0,1,.39.08,1,1,0,0,1,.53.53A1,1,0,0,1,15,2V15H2.79L1,13.2V2a1,1,0,0,1,.08-.39,1,1,0,0,1,.53-.53A1,1,0,0,1,2,1Zm0,1H13V8H3V2H2V12.79L3.2,14H4V10h7v4h3ZM4,7h8V2H4Zm6,4H5v3H6V12H7v2h3Z"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>save</title><path d="M14,1a1,1,0,0,1,.39.08,1,1,0,0,1,.53.53A1,1,0,0,1,15,2V15H2.79L1,13.2V2a1,1,0,0,1,.08-.39,1,1,0,0,1,.53-.53A1,1,0,0,1,2,1Zm0,1H13V8H3V2H2V12.79L3.2,14H4V10h7v4h3ZM4,7h8V2H4Zm6,4H5v3H6V12H7v2h3Z"/></svg>

After

Width:  |  Height:  |  Size: 323 B

View File

@@ -14,6 +14,7 @@ import { ICellModelOptions, IModelFactory, FutureInternal } from './modelInterfa
import * as notebookUtils from '../notebookUtils'; import * as notebookUtils from '../notebookUtils';
import { CellTypes, CellType, NotebookChangeType } from 'sql/parts/notebook/models/contracts'; import { CellTypes, CellType, NotebookChangeType } from 'sql/parts/notebook/models/contracts';
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces';
import { NotebookModel } from 'sql/parts/notebook/models/notebookModel';
let modelId = 0; let modelId = 0;
@@ -34,7 +35,7 @@ export class CellModel implements ICellModel {
private _active: boolean; private _active: boolean;
private _cellUri: URI; private _cellUri: URI;
constructor(private factory: IModelFactory, cellData?: nb.ICell, private _options?: ICellModelOptions) { constructor(private factory: IModelFactory, cellData?: nb.ICellContents, private _options?: ICellModelOptions) {
this.id = `${modelId++}`; this.id = `${modelId++}`;
CellModel.CreateLanguageMappings(); CellModel.CreateLanguageMappings();
// Do nothing for now // Do nothing for now
@@ -222,18 +223,48 @@ export class CellModel implements ICellModel {
if (output) { if (output) {
// deletes transient node in the serialized JSON // deletes transient node in the serialized JSON
delete output['transient']; delete output['transient'];
this._outputs.push(output); this._outputs.push(this.rewriteOutputUrls(output));
this.fireOutputsChanged(); this.fireOutputsChanged();
} }
} }
private rewriteOutputUrls(output: nb.ICellOutput): nb.ICellOutput {
// Only rewrite if this is coming back during execution, not when loading from disk.
// A good approximation is that the model has a future (needed for execution)
if (this.future) {
try {
let result = output as nb.IDisplayResult;
if (result && result.data && result.data['text/html']) {
let nbm = (this as CellModel).options.notebook as NotebookModel;
if (nbm.hadoopConnection) {
let host = nbm.hadoopConnection.host;
let html = result.data['text/html'];
html = html.replace(/(https?:\/\/mssql-master.*\/proxy)(.*)/g, function (a, b, c) {
let ret = '';
if (b !== '') {
ret = 'https://' + host + ':30443/gateway/default/yarn/proxy';
}
if (c !== '') {
ret = ret + c;
}
return ret;
});
(<nb.IDisplayResult>output).data['text/html'] = html;
}
}
}
catch (e) {}
}
return output;
}
private getDisplayId(msg: nb.IIOPubMessage): string | undefined { private getDisplayId(msg: nb.IIOPubMessage): string | undefined {
let transient = (msg.content.transient || {}); let transient = (msg.content.transient || {});
return transient['display_id'] as string; return transient['display_id'] as string;
} }
public toJSON(): nb.ICell { public toJSON(): nb.ICellContents {
let cellJson: Partial<nb.ICell> = { let cellJson: Partial<nb.ICellContents> = {
cell_type: this._cellType, cell_type: this._cellType,
source: this._source, source: this._source,
metadata: { metadata: {
@@ -244,10 +275,10 @@ export class CellModel implements ICellModel {
cellJson.outputs = this._outputs; cellJson.outputs = this._outputs;
cellJson.execution_count = 1; // TODO: keep track of actual execution count cellJson.execution_count = 1; // TODO: keep track of actual execution count
} }
return cellJson as nb.ICell; return cellJson as nb.ICellContents;
} }
public fromJSON(cell: nb.ICell): void { public fromJSON(cell: nb.ICellContents): void {
if (!cell) { if (!cell) {
return; return;
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,12 +7,12 @@
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column"> <div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div #toolbar class="editor-toolbar actionbar-container" style="flex: 0 0 auto; display: flex; flex-flow: row; width: 100%; align-items: center; height: 36px"> <div #toolbar class="editor-toolbar actionbar-container" style="flex: 0 0 auto; display: flex; flex-flow: row; width: 100%; align-items: center; height: 36px">
</div> </div>
<div class="scrollable" style="flex: 1 1 auto; position: relative"> <div class="scrollable" style="flex: 1 1 auto; position: relative" (click)="unselectActiveCell()">
<loading-spinner [loading]="isLoading"></loading-spinner> <loading-spinner [loading]="isLoading"></loading-spinner>
<div class="notebook-cell" *ngFor="let cell of cells" (click)="selectCell(cell)" [class.active]="cell.active" (keydown)="onKeyDown($event)"> <div class="notebook-cell" *ngFor="let cell of cells" (click)="selectCell(cell, $event)" [class.active]="cell.active">
<code-cell-component *ngIf="cell.cellType === 'code'" [cellModel]="cell" [model]="model"> <code-cell-component *ngIf="cell.cellType === 'code'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId">
</code-cell-component> </code-cell-component>
<text-cell-component *ngIf="cell.cellType === 'markdown'" [cellModel]="cell"> <text-cell-component *ngIf="cell.cellType === 'markdown'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId">
</text-cell-component> </text-cell-component>
</div> </div>
</div> </div>

View File

@@ -7,9 +7,8 @@ import './notebookStyles';
import { nb } from 'sqlops'; import { nb } from 'sqlops';
import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild } from '@angular/core'; import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnDestroy } from '@angular/core';
import URI from 'vs/base/common/uri';
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import * as themeColors from 'vs/workbench/common/theme'; import * as themeColors from 'vs/workbench/common/theme';
import { INotificationService, INotification } from 'vs/platform/notification/common/notification'; import { INotificationService, INotification } from 'vs/platform/notification/common/notification';
@@ -18,12 +17,12 @@ import { localize } from 'vs/nls';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { AngularDisposable } from 'sql/base/common/lifecycle'; import { AngularDisposable } from 'sql/base/common/lifecycle';
import { CellTypes, CellType, NotebookChangeType } from 'sql/parts/notebook/models/contracts'; import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts';
import { ICellModel, IModelFactory } from 'sql/parts/notebook/models/modelInterfaces'; import { ICellModel, IModelFactory, notebookConstants } from 'sql/parts/notebook/models/modelInterfaces';
import { IConnectionManagementService, IConnectionDialogService } from 'sql/parts/connection/common/connectionManagement'; import { IConnectionManagementService, IConnectionDialogService } from 'sql/parts/connection/common/connectionManagement';
import { INotebookService, INotebookParams, INotebookManager } from 'sql/services/notebook/notebookService'; import { INotebookService, INotebookParams, INotebookManager, INotebookEditor } from 'sql/services/notebook/notebookService';
import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService'; import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService';
import { NotebookModel, ErrorInfo, MessageLevel, NotebookContentChange } from 'sql/parts/notebook/models/notebookModel'; import { NotebookModel, NotebookContentChange } from 'sql/parts/notebook/models/notebookModel';
import { ModelFactory } from 'sql/parts/notebook/models/modelFactory'; import { ModelFactory } from 'sql/parts/notebook/models/modelFactory';
import * as notebookUtils from './notebookUtils'; import * as notebookUtils from './notebookUtils';
import { Deferred } from 'sql/base/common/promise'; import { Deferred } from 'sql/base/common/promise';
@@ -31,8 +30,17 @@ import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar'; import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { KernelsDropdown, AttachToDropdown, AddCellAction, TrustedAction } from 'sql/parts/notebook/notebookActions'; import { KernelsDropdown, AttachToDropdown, AddCellAction, TrustedAction, SaveNotebookAction } from 'sql/parts/notebook/notebookActions';
import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler';
import { MenuId, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions';
import { IAction, Action, IActionItem } from 'vs/base/common/actions';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { fillInActions, LabeledMenuItemActionItem } from 'vs/platform/actions/browser/menuItemActionItem';
import { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
import * as TaskUtilities from 'sql/workbench/common/taskUtilities';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
export const NOTEBOOK_SELECTOR: string = 'notebook-component'; export const NOTEBOOK_SELECTOR: string = 'notebook-component';
@@ -41,7 +49,7 @@ export const NOTEBOOK_SELECTOR: string = 'notebook-component';
selector: NOTEBOOK_SELECTOR, selector: NOTEBOOK_SELECTOR,
templateUrl: decodeURI(require.toUrl('./notebook.component.html')) templateUrl: decodeURI(require.toUrl('./notebook.component.html'))
}) })
export class NotebookComponent extends AngularDisposable implements OnInit { export class NotebookComponent extends AngularDisposable implements OnInit, OnDestroy, INotebookEditor {
@ViewChild('toolbar', { read: ElementRef }) private toolbar: ElementRef; @ViewChild('toolbar', { read: ElementRef }) private toolbar: ElementRef;
private _model: NotebookModel; private _model: NotebookModel;
private _isInErrorState: boolean = false; private _isInErrorState: boolean = false;
@@ -54,6 +62,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
private _modelRegisteredDeferred = new Deferred<NotebookModel>(); private _modelRegisteredDeferred = new Deferred<NotebookModel>();
private profile: IConnectionProfile; private profile: IConnectionProfile;
private _trustedAction: TrustedAction; private _trustedAction: TrustedAction;
private _activeCellId: string;
constructor( constructor(
@@ -61,30 +70,64 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService, @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IConnectionManagementService) private connectionManagementService: IConnectionManagementService, @Inject(IConnectionManagementService) private connectionManagementService: IConnectionManagementService,
@Inject(IObjectExplorerService) private objectExplorerService: IObjectExplorerService,
@Inject(IEditorService) private editorService: IEditorService,
@Inject(INotificationService) private notificationService: INotificationService, @Inject(INotificationService) private notificationService: INotificationService,
@Inject(INotebookService) private notebookService: INotebookService, @Inject(INotebookService) private notebookService: INotebookService,
@Inject(IBootstrapParams) private notebookParams: INotebookParams, @Inject(IBootstrapParams) private _notebookParams: INotebookParams,
@Inject(IInstantiationService) private instantiationService: IInstantiationService, @Inject(IInstantiationService) private instantiationService: IInstantiationService,
@Inject(IContextMenuService) private contextMenuService: IContextMenuService, @Inject(IContextMenuService) private contextMenuService: IContextMenuService,
@Inject(IContextViewService) private contextViewService: IContextViewService, @Inject(IContextViewService) private contextViewService: IContextViewService,
@Inject(IConnectionDialogService) private connectionDialogService: IConnectionDialogService @Inject(IConnectionDialogService) private connectionDialogService: IConnectionDialogService,
@Inject(IContextKeyService) private contextKeyService: IContextKeyService,
@Inject(IMenuService) private menuService: IMenuService,
@Inject(IKeybindingService) private keybindingService: IKeybindingService
) { ) {
super(); super();
this.profile = this.notebookParams!.profile; this.updateProfile();
this.isLoading = true; this.isLoading = true;
} }
private updateProfile(): void {
this.profile = this.notebookParams!.profile;
if (!this.profile) {
// use global connection if possible
let profile = TaskUtilities.getCurrentGlobalConnection(this.objectExplorerService, this.connectionManagementService, this.editorService);
// TODO use generic method to match kernel with valid connection that's compatible. For now, we only have 1
if (profile && profile.providerName === notebookConstants.hadoopKnoxProviderName) {
this.profile = profile;
} else {
// if not, try 1st active connection that matches our filter
let profiles = this.connectionManagementService.getActiveConnections([notebookConstants.hadoopKnoxProviderName]);
if (profiles && profiles.length > 0) {
this.profile = profiles[0];
}
}
}
}
ngOnInit() { ngOnInit() {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this)); this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme()); this.updateTheme(this.themeService.getColorTheme());
this.notebookService.addNotebookEditor(this);
this.initActionBar(); this.initActionBar();
this.doLoad(); this.doLoad();
} }
ngOnDestroy() {
if (this.notebookService) {
this.notebookService.removeNotebookEditor(this);
}
}
public get model(): NotebookModel { public get model(): NotebookModel {
return this._model; return this._model;
} }
public get activeCellId(): string {
return this._activeCellId;
}
public get modelRegistered(): Promise<NotebookModel> { public get modelRegistered(): Promise<NotebookModel> {
return this._modelRegisteredDeferred.promise; return this._modelRegisteredDeferred.promise;
} }
@@ -98,7 +141,10 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
toolbarEl.style.borderBottomColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString(); toolbarEl.style.borderBottomColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
} }
public selectCell(cell: ICellModel) { public selectCell(cell: ICellModel, event?: Event) {
if (event) {
event.stopPropagation();
}
if (cell !== this._activeCell) { if (cell !== this._activeCell) {
if (this._activeCell) { if (this._activeCell) {
this._activeCell.active = false; this._activeCell.active = false;
@@ -106,10 +152,21 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
this._activeCell = cell; this._activeCell = cell;
this._activeCell.active = true; this._activeCell.active = true;
this._model.activeCell = this._activeCell; this._model.activeCell = this._activeCell;
this._activeCellId = cell.id;
this._changeRef.detectChanges(); this._changeRef.detectChanges();
} }
} }
public unselectActiveCell() {
if (this._activeCell) {
this._activeCell.active = false;
}
this._activeCell = null;
this._model.activeCell = null;
this._activeCellId = null;
this._changeRef.detectChanges();
}
// Add cell based on cell type // Add cell based on cell type
public addCell(cellType: CellType) public addCell(cellType: CellType)
{ {
@@ -165,16 +222,16 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
} }
private async loadModel(): Promise<void> { private async loadModel(): Promise<void> {
this.notebookManager = await this.notebookService.getOrCreateNotebookManager(this.notebookParams.providerId, this.notebookParams.notebookUri); this.notebookManager = await this.notebookService.getOrCreateNotebookManager(this._notebookParams.providerId, this._notebookParams.notebookUri);
let model = new NotebookModel({ let model = new NotebookModel({
factory: this.modelFactory, factory: this.modelFactory,
notebookUri: this.notebookParams.notebookUri, notebookUri: this._notebookParams.notebookUri,
connectionService: this.connectionManagementService, connectionService: this.connectionManagementService,
notificationService: this.notificationService, notificationService: this.notificationService,
notebookManager: this.notebookManager notebookManager: this.notebookManager
}, false, this.profile); }, false, this.profile);
model.onError((errInfo: INotification) => this.handleModelError(errInfo)); model.onError((errInfo: INotification) => this.handleModelError(errInfo));
await model.requestModelLoad(this.notebookParams.isTrusted); await model.requestModelLoad(this._notebookParams.isTrusted);
model.contentChanged((change) => this.handleContentChanged(change)); model.contentChanged((change) => this.handleContentChanged(change));
this._model = model; this._model = model;
this.updateToolbarComponents(this._model.trustedMode); this.updateToolbarComponents(this._model.trustedMode);
@@ -195,10 +252,10 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
} }
private get modelFactory(): IModelFactory { private get modelFactory(): IModelFactory {
if (!this.notebookParams.modelFactory) { if (!this._notebookParams.modelFactory) {
this.notebookParams.modelFactory = new ModelFactory(); this._notebookParams.modelFactory = new ModelFactory();
} }
return this.notebookParams.modelFactory; return this._notebookParams.modelFactory;
} }
private handleModelError(notification: INotification): void { private handleModelError(notification: INotification): void {
this.notificationService.notify(notification); this.notificationService.notify(notification);
@@ -233,9 +290,6 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
attachTodropdwon.render(attachToContainer); attachTodropdwon.render(attachToContainer);
attachSelectBoxStyler(attachTodropdwon, this.themeService); attachSelectBoxStyler(attachTodropdwon, this.themeService);
let attachToInfoText = document.createElement('div');
attachToInfoText.className = 'notebook-info-label';
attachToInfoText.innerText = 'Attach To: ';
let addCodeCellButton = new AddCellAction('notebook.AddCodeCell', localize('code', 'Code'), 'notebook-button icon-add'); let addCodeCellButton = new AddCellAction('notebook.AddCodeCell', localize('code', 'Code'), 'notebook-button icon-add');
addCodeCellButton.cellType = CellTypes.Code; addCodeCellButton.cellType = CellTypes.Code;
@@ -246,21 +300,44 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
this._trustedAction = this.instantiationService.createInstance(TrustedAction, 'notebook.Trusted'); this._trustedAction = this.instantiationService.createInstance(TrustedAction, 'notebook.Trusted');
this._trustedAction.enabled = false; this._trustedAction.enabled = false;
let saveNotebookButton = this.instantiationService.createInstance(SaveNotebookAction, 'notebook.SaveNotebook', localize('save', 'Save'), 'notebook-button icon-save');
// Get all of the menu contributions that use the ID 'notebook/toolbar'.
// Then, find all groups (currently we don't leverage the contributed
// groups functionality for the notebook toolbar), and fill in the 'primary'
// array with items that don't list a group. Finally, add any actions from
// the primary array to the end of the toolbar.
const notebookBarMenu = this.menuService.createMenu(MenuId.NotebookToolbar, this.contextKeyService);
let groups = notebookBarMenu.getActions({ arg: null, shouldForwardArgs: true });
let primary: IAction[] = [];
let secondary: IAction[] = [];
fillInActions(groups, {primary, secondary}, false, (group: string) => group === undefined);
let taskbar = <HTMLElement>this.toolbar.nativeElement; let taskbar = <HTMLElement>this.toolbar.nativeElement;
this._actionBar = new Taskbar(taskbar, this.contextMenuService); this._actionBar = new Taskbar(taskbar, this.contextMenuService, { actionItemProvider: action => this.actionItemProvider(action as Action)});
this._actionBar.context = this; this._actionBar.context = this;
this._actionBar.setContent([ this._actionBar.setContent([
{ element: kernelContainer }, { element: kernelContainer },
{ element: attachToContainer }, { element: attachToContainer },
{ action: addCodeCellButton}, { action: addCodeCellButton },
{ action: addTextCellButton}, { action: addTextCellButton },
{ action: this._trustedAction} { action: saveNotebookButton },
{ action: this._trustedAction }
]); ]);
// Primary actions are categorized as those that are added to the 'horizontal' group.
// For the vertical toolbar, we can do the same thing and instead use the 'vertical' group.
for (let action of primary) {
this._actionBar.addAction(action);
}
} }
public async save(): Promise<boolean> { public async save(): Promise<boolean> {
try { try {
let saved = await this._model.saveModel(); let saved = await this._model.saveModel();
if (saved) {
this.setDirty(false);
}
return saved; return saved;
} catch (err) { } catch (err) {
this.notificationService.error(localize('saveFailed', 'Failed to save notebook: {0}', notebookUtils.getErrorMessage(err))); this.notificationService.error(localize('saveFailed', 'Failed to save notebook: {0}', notebookUtils.getErrorMessage(err)));
@@ -269,11 +346,46 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
} }
private setDirty(isDirty: boolean): void { private setDirty(isDirty: boolean): void {
// TODO reenable handling of isDirty if(this._notebookParams.input){
// if (this.editor) { this._notebookParams.input.setDirty(isDirty);
// this.editor.isDirty = isDirty; }
// }
} }
private actionItemProvider(action: Action): IActionItem {
// Check extensions to create ActionItem; otherwise, return undefined
// This is similar behavior that exists in MenuItemActionItem
if (action instanceof MenuItemAction) {
return new LabeledMenuItemActionItem(action, this.keybindingService, this.notificationService, this.contextMenuService, 'notebook-button');
}
return undefined;
}
public get notebookParams(): INotebookParams {
return this._notebookParams;
}
public get id(): string {
return this._notebookParams.notebookUri.toString();
}
isActive(): boolean {
return this.editorService.activeEditor === this.notebookParams.input;
}
isVisible(): boolean {
let notebookEditor = this.notebookParams.input;
return this.editorService.visibleEditors.some(e => e === notebookEditor);
}
isDirty(): boolean {
return this.notebookParams.input.isDirty();
}
executeEdits(edits: ISingleNotebookEditOperation[]): boolean {
if (!edits || edits.length === 0) {
return false;
}
this._model.pushEditOperations(edits);
return true;
}
} }

Some files were not shown because too many files have changed in this diff Show More