Compare commits

...

25 Commits

Author SHA1 Message Date
Karl Burtram
8c2d79e9cf Update SQL Tools to 1.4.0-alpha.25 (#1304) 2018-04-30 22:48:17 -07:00
Matt Irvine
cd140b5527 Encode HTML when entered in edit data cells (#1302)
* Encode HTML when entered in edit data cells

* Use VS Code's string encoding function
2018-04-30 17:58:39 -07:00
Karl Burtram
a0456bf4f7 Remove all ID fields from telemetry (#1299)
* Remove all ID fields from telemetry

* Fix how some of the fields are blanked out

* Add back blank userId

* Disable VS Code tests broken by this change
2018-04-30 16:52:54 -07:00
Karl Burtram
55e3947cf7 Pick up Electron 1.7.12 (#1291)
* Pick up Electron 1.7.12

* Might as well go to 1.7.13

* Go back to 1.7.12 to match VS Code
2018-04-30 09:18:02 -07:00
Karl Burtram
db5156e4cd Fix app name in Report Issue dialog (#1292) 2018-04-29 13:19:09 -07:00
Leila Lali
eece0677a7 added libunwind8 to Debian dependencies (#1290)
* added libunwind8 to debian depedencies
2018-04-27 17:08:26 -07:00
Matt Irvine
24e8c20511 Simplify button logic and enable button updates for custom dialogs (#1283) 2018-04-27 16:29:18 -07:00
Leila Lali
886717d330 added dropdown and form layout to model view (#1269)
* added dropdown and form layout to model view
2018-04-27 15:43:23 -07:00
Kevin Cunnane
26b27a616a Refreshing package-lock for samples since it's stale (#1287) 2018-04-27 10:19:11 -07:00
Karl Burtram
0c663e5555 Add AppInsights context flag to envelope (#1282)
* Add AppInsights context flag to envelope

* Fix typo

* Use add processor instead of patching
2018-04-26 21:26:51 -07:00
Aditya Bist
0f087915f6 Agent/loading icon (#1263)
* added progress wheel to show jobs loading

* added loading wheel to jobs view page too
2018-04-26 15:01:23 -07:00
Anthony Dresser
a78fa9c0f2 Scroll properties (#1244)
* properties isn't scrolling

* working on edge cases

* formatting

* formatting

* formatting
2018-04-26 14:10:08 -07:00
Karl Burtram
b1752ea635 Show all available extensions in Extension Manager (#1273)
* Show all available extensions in Extension Manager

* Change name of functions

* Minor cleanup
2018-04-26 10:08:02 -07:00
Matt Irvine
ec150917c2 Fix button handle bug and add tests (#1267) 2018-04-25 16:22:54 -07:00
Karl Burtram
7a9a69c439 Add TRUNCATE to the keyword colorization list (#1270)
* Add TRUNCATE to the keyword colorization list

* Add DISTINCT to keyword list
2018-04-25 16:07:46 -07:00
Aditya Bist
9e9862c6f0 Agent action icons fix (#1255)
* change icon opacity based on job status

* rid of magic numbers
2018-04-25 12:20:46 -07:00
Karl Burtram
7b76d929cd Update README and CHANGELOG for April release (#1257) 2018-04-24 22:39:01 -07:00
Matt Irvine
1811dfa423 Expose custom dialog extension APIs (#1206) 2018-04-24 16:43:14 -07:00
Karl Burtram
3abbc8fd97 Bump SQL Ops Studio to 0.28.6 (#1252) 2018-04-24 13:58:09 -07:00
Abbie Petchtes
4b88b67bed update Server Reports README (#1250) 2018-04-24 13:49:56 -07:00
Aditya Bist
a2734807ca Agent bugs and fixes (#1243)
* show Error for temp placeholder

* changed message to loading error

* changed error message when loading

* localized error msg

* localized error message
2018-04-24 13:34:17 -07:00
Abbie Petchtes
d0d4df313e Initial improvement in sp_whoisactive (#1249)
* improve sp_whoisactive extension

* formatting

* use a grouping prefix for extension commands
2018-04-24 13:09:50 -07:00
Abbie Petchtes
1efd5e6502 change keyboard shortcut for focusOnCurrentQuery (#1241) 2018-04-24 13:09:32 -07:00
Abbie Petchtes
b12cac0ac3 Changed DB Space Usage and DB Buffer Usage to show only top 10 data and update README (#1248) 2018-04-24 13:09:13 -07:00
Aditya Bist
578aa6ccd2 removed test code 2018-04-24 11:04:09 -07:00
70 changed files with 5001 additions and 2011 deletions

View File

@@ -1,3 +1,3 @@
disturl "https://atom.io/download/electron" disturl "https://atom.io/download/electron"
target "1.7.11" target "1.7.12"
runtime "electron" runtime "electron"

View File

@@ -1,5 +1,20 @@
# Change Log # Change Log
## Version 0.28.6
* Release date: April 25, 2018
* Release status: Public Preview
## What's new in this version
The April Public Preview release contains some of the following highlights.
* Improvements to SQL Agent *Preview* extension
* Accessibility improvements for keyboard navigation, screen reader support and high-contrast mode.
* Improved large and protected file support for saving Admin protected and >256M files within SQL Ops Studio
* Integrated Terminal splitting to work with multiple open terminals at once
* Reduced installation on-disk file count foot print for faster installs and startup times
* Improvements to Server Reports extension
* Continue to fix GitHub issues
## Version 0.27.3 ## Version 0.27.3
* Release date: March 28, 2017 * Release date: March 28, 2017
* Release status: Public Preview * Release status: Public Preview

View File

@@ -8,12 +8,12 @@ SQL Operations Studio is a data management tool that enables you to work with SQ
Platform | Link Platform | Link
-- | -- -- | --
Windows Setup Installer | https://go.microsoft.com/fwlink/?linkid=870837 Windows Setup Installer | https://go.microsoft.com/fwlink/?linkid=872717
Windows ZIP | https://go.microsoft.com/fwlink/?linkid=870838 Windows ZIP | https://go.microsoft.com/fwlink/?linkid=872718
macOS ZIP | https://go.microsoft.com/fwlink/?linkid=870839 macOS ZIP | https://go.microsoft.com/fwlink/?linkid=872719
Linux TAR.GZ | https://go.microsoft.com/fwlink/?linkid=870840 Linux TAR.GZ | https://go.microsoft.com/fwlink/?linkid=872720
Linux DEB | https://go.microsoft.com/fwlink/?linkid=870842 Linux DEB | https://go.microsoft.com/fwlink/?linkid=872722
Linux RPM | https://go.microsoft.com/fwlink/?linkid=870841 Linux RPM | https://go.microsoft.com/fwlink/?linkid=872721
Go to our [download page](https://aka.ms/sqlopsstudio) for more specific instructions. Go to our [download page](https://aka.ms/sqlopsstudio) for more specific instructions.
@@ -21,14 +21,6 @@ Try out the latest insiders build from `master` at https://github.com/Microsoft/
See the [change log](https://github.com/Microsoft/sqlopsstudio/blob/master/CHANGELOG.md) for additional details of what's in this release. See the [change log](https://github.com/Microsoft/sqlopsstudio/blob/master/CHANGELOG.md) for additional details of what's in this release.
**Design Discussions**
The SQL Operations Studio team would like to incorporate community feedback earlier in the development process. To facilitate this, we'd like to share our designs while features are actively being built.
We're currently collecting input on the **SQL Agent** experience and enhancements to the Manage Dashboard that we're calling **"Command Center"**. We'll add additional design feedback requests below as we start work in new feature areas. Please leave comments on these issues to help us understand your requirements and shape feature development.
* [#750 Seeking community feedback on SQL Agent UX prototype](https://github.com/Microsoft/sqlopsstudio/issues/750)
**Feature Highlights** **Feature Highlights**
- Cross-Platform DB management for Windows, macOS and Linux with simple XCopy deployment - Cross-Platform DB management for Windows, macOS and Linux with simple XCopy deployment

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.4.0-alpha.23", "version": "1.4.0-alpha.25",
"downloadFileNames": { "downloadFileNames": {
"Windows_86": "win-x86-netcoreapp2.1.zip", "Windows_86": "win-x86-netcoreapp2.1.zip",
"Windows_64": "win-x64-netcoreapp2.1.zip", "Windows_64": "win-x64-netcoreapp2.1.zip",

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{ {
"name": "sqlops", "name": "sqlops",
"version": "0.28.5", "version": "0.29.1",
"distro": "8c3e97e3425cc9814496472ab73e076de2ba99ee", "distro": "8c3e97e3425cc9814496472ab73e076de2ba99ee",
"author": { "author": {
"name": "Microsoft Corporation" "name": "Microsoft Corporation"

View File

@@ -1,7 +1,7 @@
Package: @@NAME@@ Package: @@NAME@@
Version: @@VERSION@@ Version: @@VERSION@@
Section: devel Section: devel
Depends: libnotify4, libnss3, gnupg, apt, libxkbfile1, libgconf-2-4, libsecret-1-0 Depends: libnotify4, libnss3, gnupg, apt, libxkbfile1, libgconf-2-4, libsecret-1-0, libunwind8
Priority: optional Priority: optional
Architecture: @@ARCHITECTURE@@ Architecture: @@ARCHITECTURE@@
Maintainer: Microsoft Corporation Maintainer: Microsoft Corporation

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,57 @@
// A launch configuration that launches the extension inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
// To debug the extension:
// 1. please install the "SQL Operations Studio Debug" extension into VSCode
// 2. Ensure sqlops is added to your path:
// - open SQL Operations Studio
// - run the command "Install 'sqlops' command in PATH"
{ {
// Use IntelliSense to learn about possible attributes. "version": "0.2.0",
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [ "configurations": [
{ {
"type": "node", "name": "Debug in SqlOps install",
"type": "sqlopsExtensionHost",
"request": "launch", "request": "launch",
"name": "Launch Program", "runtimeExecutable": "sqlops",
"program": "${workspaceFolder}\\out\\src\\extension" "args": [
"--extensionDevelopmentPath=${workspaceFolder}"
]
}, },
{ {
"type": "node", "type": "node",
"request": "attach", "request": "attach",
"name": "Attach to Ops Studio", "name": "Attach to Ops Studio",
"protocol": "inspector", "protocol": "inspector",
"port": 5870, "port": 5870,
"restart": true, "restart": true,
"sourceMaps": true, "sourceMaps": true,
"outFiles": [ "outFiles": [
"${workspaceRoot}/out/**/*.js" "${workspaceRoot}/out/**/*.js"
], ],
"preLaunchTask": "", "preLaunchTask": "",
"timeout": 25000 "timeout": 25000
}, },
{
"name": "Debug in enlistment",
"type": "sqlopsExtensionHost",
"request": "launch",
"windows": {
"runtimeExecutable": "${workspaceFolder}/../../scripts/sql.bat"
},
"osx": {
"runtimeExecutable": "${workspaceFolder}/../../scripts/sql.sh"
},
"linux": {
"runtimeExecutable": "${workspaceFolder}/../../scripts/sql.sh"
},
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"timeout": 20000
}
] ]
} }

View File

@@ -22,4 +22,22 @@ See the [Server Reports Extension Project] in the SQL Operations Studio reposito
## Contributions and "thank you" ## Contributions and "thank you"
Special thank to Paul Randal, Aaron Bertrand, and Glenn Berry for providing useful queries. Special thanks to our Microsoft MVPs for providing useful queries.
* Paul Randal:
https://www.sqlskills.com/blogs/paul/wait-statistics-or-please-tell-me-where-it-hurts/
See [Paul Randal's wait types library] for more information about each wait type in the Wait Counts widget.
[Paul Randal's wait types library]:https://www.sqlskills.com/help/waits
* Glenn Berry: https://gallery.technet.microsoft.com/scriptcenter/All-Databases-Data-log-a36da95d
* Aaron Bertrand: https://www.mssqltips.com/sqlservertip/2393/determine-sql-server-memory-use-by-database-and-object/
We would like to thank all our users who raised issues, and in particular the following users who helped contribute fixes:
* flyfishingdba for Add square brackets for ms_foreachdb call (#1023)
## What's new in Server Reports v1.1?
* Fixed DB Space Usage where it threw an error when database names contain special characters
* Changed DB Space Usage and DB Buffer Usage to show only top 10 data

View File

@@ -625,6 +625,7 @@
"anymatch": "2.0.0", "anymatch": "2.0.0",
"async-each": "1.0.1", "async-each": "1.0.1",
"braces": "2.3.1", "braces": "2.3.1",
"fsevents": "1.2.2",
"glob-parent": "3.1.0", "glob-parent": "3.1.0",
"inherits": "2.0.3", "inherits": "2.0.3",
"is-binary-path": "1.0.1", "is-binary-path": "1.0.1",
@@ -1730,6 +1731,535 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
}, },
"fsevents": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.2.tgz",
"integrity": "sha512-iownA+hC4uHFp+7gwP/y5SzaiUo7m2vpa0dhpzw8YuKtiZsz7cIXsFbXpLEeBM6WuCQyw1MH4RRe6XI8GFUctQ==",
"dev": true,
"optional": true,
"requires": {
"nan": "2.10.0",
"node-pre-gyp": "0.9.1"
},
"dependencies": {
"abbrev": {
"version": "1.1.1",
"bundled": true,
"dev": true,
"optional": true
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
},
"aproba": {
"version": "1.2.0",
"bundled": true,
"dev": true,
"optional": true
},
"are-we-there-yet": {
"version": "1.1.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"delegates": "1.0.0",
"readable-stream": "2.3.6"
}
},
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"requires": {
"balanced-match": "1.0.0",
"concat-map": "0.0.1"
}
},
"chownr": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
},
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
},
"core-util-is": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
},
"debug": {
"version": "2.6.9",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ms": "2.0.0"
}
},
"deep-extend": {
"version": "0.4.2",
"bundled": true,
"dev": true,
"optional": true
},
"delegates": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
},
"detect-libc": {
"version": "1.0.3",
"bundled": true,
"dev": true,
"optional": true
},
"fs-minipass": {
"version": "1.2.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minipass": "2.2.4"
}
},
"fs.realpath": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
},
"gauge": {
"version": "2.7.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"aproba": "1.2.0",
"console-control-strings": "1.1.0",
"has-unicode": "2.0.1",
"object-assign": "4.1.1",
"signal-exit": "3.0.2",
"string-width": "1.0.2",
"strip-ansi": "3.0.1",
"wide-align": "1.1.2"
}
},
"glob": {
"version": "7.1.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"fs.realpath": "1.0.0",
"inflight": "1.0.6",
"inherits": "2.0.3",
"minimatch": "3.0.4",
"once": "1.4.0",
"path-is-absolute": "1.0.1"
}
},
"has-unicode": {
"version": "2.0.1",
"bundled": true,
"dev": true,
"optional": true
},
"iconv-lite": {
"version": "0.4.21",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safer-buffer": "2.1.2"
}
},
"ignore-walk": {
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimatch": "3.0.4"
}
},
"inflight": {
"version": "1.0.6",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"once": "1.4.0",
"wrappy": "1.0.2"
}
},
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
},
"ini": {
"version": "1.3.5",
"bundled": true,
"dev": true,
"optional": true
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"requires": {
"number-is-nan": "1.0.1"
}
},
"isarray": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
},
"minimatch": {
"version": "3.0.4",
"bundled": true,
"dev": true,
"requires": {
"brace-expansion": "1.1.11"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"dev": true,
"requires": {
"safe-buffer": "5.1.1",
"yallist": "3.0.2"
}
},
"minizlib": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minipass": "2.2.4"
}
},
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"dev": true,
"requires": {
"minimist": "0.0.8"
}
},
"ms": {
"version": "2.0.0",
"bundled": true,
"dev": true,
"optional": true
},
"needle": {
"version": "2.2.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"debug": "2.6.9",
"iconv-lite": "0.4.21",
"sax": "1.2.4"
}
},
"node-pre-gyp": {
"version": "0.9.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"detect-libc": "1.0.3",
"mkdirp": "0.5.1",
"needle": "2.2.0",
"nopt": "4.0.1",
"npm-packlist": "1.1.10",
"npmlog": "4.1.2",
"rc": "1.2.6",
"rimraf": "2.6.2",
"semver": "5.5.0",
"tar": "4.4.1"
}
},
"nopt": {
"version": "4.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"abbrev": "1.1.1",
"osenv": "0.1.5"
}
},
"npm-bundled": {
"version": "1.0.3",
"bundled": true,
"dev": true,
"optional": true
},
"npm-packlist": {
"version": "1.1.10",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ignore-walk": "3.0.1",
"npm-bundled": "1.0.3"
}
},
"npmlog": {
"version": "4.1.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"are-we-there-yet": "1.1.4",
"console-control-strings": "1.1.0",
"gauge": "2.7.4",
"set-blocking": "2.0.0"
}
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
},
"object-assign": {
"version": "4.1.1",
"bundled": true,
"dev": true,
"optional": true
},
"once": {
"version": "1.4.0",
"bundled": true,
"dev": true,
"requires": {
"wrappy": "1.0.2"
}
},
"os-homedir": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
},
"os-tmpdir": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
},
"osenv": {
"version": "0.1.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"os-homedir": "1.0.2",
"os-tmpdir": "1.0.2"
}
},
"path-is-absolute": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
},
"process-nextick-args": {
"version": "2.0.0",
"bundled": true,
"dev": true,
"optional": true
},
"rc": {
"version": "1.2.6",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"deep-extend": "0.4.2",
"ini": "1.3.5",
"minimist": "1.2.0",
"strip-json-comments": "2.0.1"
},
"dependencies": {
"minimist": {
"version": "1.2.0",
"bundled": true,
"dev": true,
"optional": true
}
}
},
"readable-stream": {
"version": "2.3.6",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "2.0.0",
"safe-buffer": "5.1.1",
"string_decoder": "1.1.1",
"util-deprecate": "1.0.2"
}
},
"rimraf": {
"version": "2.6.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"glob": "7.1.2"
}
},
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
"bundled": true,
"dev": true,
"optional": true
},
"sax": {
"version": "1.2.4",
"bundled": true,
"dev": true,
"optional": true
},
"semver": {
"version": "5.5.0",
"bundled": true,
"dev": true,
"optional": true
},
"set-blocking": {
"version": "2.0.0",
"bundled": true,
"dev": true,
"optional": true
},
"signal-exit": {
"version": "3.0.2",
"bundled": true,
"dev": true,
"optional": true
},
"string-width": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"requires": {
"code-point-at": "1.1.0",
"is-fullwidth-code-point": "1.0.0",
"strip-ansi": "3.0.1"
}
},
"string_decoder": {
"version": "1.1.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "5.1.1"
}
},
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"dev": true,
"requires": {
"ansi-regex": "2.1.1"
}
},
"strip-json-comments": {
"version": "2.0.1",
"bundled": true,
"dev": true,
"optional": true
},
"tar": {
"version": "4.4.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"chownr": "1.0.1",
"fs-minipass": "1.2.5",
"minipass": "2.2.4",
"minizlib": "1.1.0",
"mkdirp": "0.5.1",
"safe-buffer": "5.1.1",
"yallist": "3.0.2"
}
},
"util-deprecate": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
},
"wide-align": {
"version": "1.1.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"string-width": "1.0.2"
}
},
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
},
"yallist": {
"version": "3.0.2",
"bundled": true,
"dev": true
}
}
},
"fstream": { "fstream": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
@@ -4105,6 +4635,13 @@
"integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
"dev": true "dev": true
}, },
"nan": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
"dev": true,
"optional": true
},
"nanomatch": { "nanomatch": {
"version": "1.2.9", "version": "1.2.9",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz",

View File

@@ -2,7 +2,7 @@
"name": "server-report", "name": "server-report",
"displayName": "Server Reports", "displayName": "Server Reports",
"description": "Server Reports", "description": "Server Reports",
"version": "0.1.0", "version": "0.1.1",
"publisher": "Microsoft", "publisher": "Microsoft",
"preview": true, "preview": true,
"engines": { "engines": {
@@ -126,15 +126,25 @@
"container": { "container": {
"widgets-container": [ "widgets-container": [
{ {
"name": "DB Space Usage", "name": "Top 10 DB Space Usage",
"gridItemConfig": { "gridItemConfig": {
"sizex": 2, "sizex": 2,
"sizey": 1 "sizey": 2
}, },
"widget": { "widget": {
"extension-dbspace-usage": {} "extension-dbspace-usage": {}
} }
}, },
{
"name": "Top 10 DB Buffer Usage",
"gridItemConfig": {
"sizex": 2,
"sizey": 2
},
"widget": {
"extension-dbbuffer-usage": {}
}
},
{ {
"name": "CPU Utilization", "name": "CPU Utilization",
"gridItemConfig": { "gridItemConfig": {
@@ -164,16 +174,6 @@
"widget": { "widget": {
"extension-wait-counts-by-Paul-Randal": {} "extension-wait-counts-by-Paul-Randal": {}
} }
},
{
"name": "DB Buffer Usage",
"gridItemConfig": {
"sizex": 2,
"sizey": 2
},
"widget": {
"extension-dbbuffer-usage": {}
}
} }
] ]
} }

View File

@@ -89,7 +89,7 @@ FROM (
ON p.object_id = it.object_id ON p.object_id = it.object_id
) AS partitions' ) AS partitions'
----------------------------------- -----------------------------------
select select TOP 10
d.Dbname, d.Dbname,
--(file_size_mb + log_file_size_mb) as DBsize, --(file_size_mb + log_file_size_mb) as DBsize,
--d.file_Size_MB, --d.file_Size_MB,
@@ -100,4 +100,4 @@ select
--l.log_Free_Space_MB, --l.log_Free_Space_MB,
--fs.Freespace as DB_Freespace --fs.Freespace as DB_Freespace
from @dbsize d join @logsize l on d.Dbname=l.Dbname join @dbfreesize fs on d.Dbname=fs.name from @dbsize d join @logsize l on d.Dbname=l.Dbname join @dbfreesize fs on d.Dbname=fs.name
order by Dbname order by d.Space_Used_MB DESC

View File

@@ -16,7 +16,7 @@ FROM sys.dm_os_buffer_descriptors
--WHERE database_id BETWEEN 5 AND 32766 --WHERE database_id BETWEEN 5 AND 32766
GROUP BY database_id GROUP BY database_id
) )
SELECT SELECT TOP 10
[db_name] = CASE [database_id] WHEN 32767 [db_name] = CASE [database_id] WHEN 32767
THEN 'Resource DB' THEN 'Resource DB'
ELSE DB_NAME([database_id]) END, ELSE DB_NAME([database_id]) END,

View File

@@ -1,6 +1,6 @@
{ {
"name": "whoisactive", "name": "whoisactive",
"version": "0.1.0", "version": "0.1.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@@ -2,7 +2,7 @@
"name": "whoisactive", "name": "whoisactive",
"displayName": "whoisactive", "displayName": "whoisactive",
"description": "sp_whoisactive for SQL Operations Studio", "description": "sp_whoisactive for SQL Operations Studio",
"version": "0.1.0", "version": "0.1.1",
"publisher": "Microsoft", "publisher": "Microsoft",
"preview": true, "preview": true,
"engines": { "engines": {
@@ -24,7 +24,7 @@
"commands": [ "commands": [
{ {
"command": "sp_whoisactive.install", "command": "sp_whoisactive.install",
"title": "Install sp_whoisactive", "title": "Whoisactive: Install sp_whoisactive",
"icon": { "icon": {
"light": "./out/src/media/download.svg", "light": "./out/src/media/download.svg",
"dark": "./out/src/media/download_inverse.svg" "dark": "./out/src/media/download_inverse.svg"
@@ -32,7 +32,7 @@
}, },
{ {
"command": "sp_whoisactive.findBlockLeaders", "command": "sp_whoisactive.findBlockLeaders",
"title": "Find leader of block", "title": "Whoisactive: Find leader of block",
"icon": { "icon": {
"light": "./out/src/media/blocker.svg", "light": "./out/src/media/blocker.svg",
"dark": "./out/src/media/blocker_inverse.svg" "dark": "./out/src/media/blocker_inverse.svg"
@@ -40,11 +40,19 @@
}, },
{ {
"command": "sp_whoisactive.getPlans", "command": "sp_whoisactive.getPlans",
"title": "Get plans", "title": "Whoisactive: Get plans",
"icon": { "icon": {
"light": "./out/src/media/monitor.svg", "light": "./out/src/media/monitor.svg",
"dark": "./out/src/media/monitor_inverse.svg" "dark": "./out/src/media/monitor_inverse.svg"
} }
},
{
"command": "sp_whoisactive.documentation",
"title": "Whoisactive: Documentation",
"icon": {
"light": "./out/src/media/documentation.svg",
"dark": "./out/src/media/documentation_inverse.svg"
}
} }
], ],
"views": {}, "views": {},
@@ -55,30 +63,7 @@
"title": "sp_whoisactive", "title": "sp_whoisactive",
"description": "Extension for checking who is active.", "description": "Extension for checking who is active.",
"container": { "container": {
"nav-section": [ "sp_whoisactive-insights": {}
{
"id": "sp_whoisactive_insights",
"title": "Insights",
"icon": {
"light": "./out/src/media/insights.svg",
"dark": "./out/src/media/insights_inverse.svg"
},
"container": {
"sp_whoisactive-insights": {}
}
},
{
"id": "sp_whoisactive_documentation",
"title": "Documentation",
"icon": {
"light": "./out/src/media/documentation.svg",
"dark": "./out/src/media/documentation_inverse.svg"
},
"container": {
"webview-container": null
}
}
]
} }
} }
], ],
@@ -92,7 +77,8 @@
"dataType": "number", "dataType": "number",
"legendPosition": "none", "legendPosition": "none",
"labelFirstColumn": false, "labelFirstColumn": false,
"columnsAsLabels": true "columnsAsLabels": true,
"showTopNData": 5
} }
}, },
"queryFile": "./out/src/sql/cpuUsage.sql" "queryFile": "./out/src/sql/cpuUsage.sql"
@@ -107,7 +93,8 @@
"dataType": "number", "dataType": "number",
"legendPosition": "none", "legendPosition": "none",
"labelFirstColumn": false, "labelFirstColumn": false,
"columnsAsLabels": true "columnsAsLabels": true,
"showTopNData": 5
} }
}, },
"queryFile": "./out/src/sql/cpuDelta.sql" "queryFile": "./out/src/sql/cpuDelta.sql"
@@ -122,7 +109,8 @@
"dataType": "number", "dataType": "number",
"legendPosition": "none", "legendPosition": "none",
"labelFirstColumn": false, "labelFirstColumn": false,
"columnsAsLabels": true "columnsAsLabels": true,
"showTopNData": 5
} }
}, },
"queryFile": "./out/src/sql/memoryUsage.sql" "queryFile": "./out/src/sql/memoryUsage.sql"
@@ -137,11 +125,21 @@
"dataType": "number", "dataType": "number",
"legendPosition": "none", "legendPosition": "none",
"labelFirstColumn": false, "labelFirstColumn": false,
"columnsAsLabels": true "columnsAsLabels": true,
"showTopNData": 5
} }
}, },
"queryFile": "./out/src/sql/memoryDelta.sql" "queryFile": "./out/src/sql/memoryDelta.sql"
} }
},
{
"id": "sp_whoisactive-blocking_sessions",
"contrib": {
"type": {
"table": null
},
"queryFile": "./out/src/sql/blockingSessions.sql"
}
} }
], ],
"dashboard.containers": [ "dashboard.containers": [
@@ -155,12 +153,13 @@
"tasks-widget": [ "tasks-widget": [
"sp_whoisactive.getPlans", "sp_whoisactive.getPlans",
"sp_whoisactive.findBlockLeaders", "sp_whoisactive.findBlockLeaders",
"sp_whoisactive.documentation",
"sp_whoisactive.install" "sp_whoisactive.install"
] ]
} }
}, },
{ {
"name": "CPU Usage", "name": "Top 5 CPU Usage",
"gridItemConfig": { "gridItemConfig": {
"sizex": 2, "sizex": 2,
"sizey": 1 "sizey": 1
@@ -170,7 +169,7 @@
} }
}, },
{ {
"name": "CPU Delta", "name": "Top 5 CPU Delta",
"gridItemConfig": { "gridItemConfig": {
"sizex": 2, "sizex": 2,
"sizey": 1 "sizey": 1
@@ -180,7 +179,7 @@
} }
}, },
{ {
"name": "Memory Usage", "name": "Top 5 Memory Usage",
"gridItemConfig": { "gridItemConfig": {
"sizex": 2, "sizex": 2,
"sizey": 1 "sizey": 1
@@ -190,7 +189,7 @@
} }
}, },
{ {
"name": "Memory Delta", "name": "Top 5 Memory Delta",
"gridItemConfig": { "gridItemConfig": {
"sizex": 2, "sizex": 2,
"sizey": 1 "sizey": 1
@@ -198,6 +197,16 @@
"widget": { "widget": {
"sp_whoisactive-memory-delta": {} "sp_whoisactive-memory-delta": {}
} }
},
{
"name": "Blocking Sessions",
"gridItemConfig": {
"sizex": 2,
"sizey": 1
},
"widget": {
"sp_whoisactive-blocking_sessions": {}
}
} }
] ]
} }

View File

@@ -29,23 +29,16 @@ export default class MainController extends ControllerBase {
} }
public activate(): Promise<boolean> { public activate(): Promise<boolean> {
sqlops.dashboard.registerWebviewProvider('sp_whoisactive_documentation', webview => { sqlops.tasks.registerTask('sp_whoisactive.install', e => this.openurl('http://whoisactive.com/downloads/'));
let templateValues = {url: 'http://whoisactive.com/docs/'}; sqlops.tasks.registerTask('sp_whoisactive.documentation', e => this.openurl('http://whoisactive.com/docs/'));
Utils.renderTemplateHtml(path.join(__dirname, '..'), 'templateTab.html', templateValues)
.then(html => {
webview.html = html;
});
});
sqlops.tasks.registerTask('sp_whoisactive.install', e => this.onInstall(e));
sqlops.tasks.registerTask('sp_whoisactive.findBlockLeaders', e => this.onExecute(e, 'findBlockLeaders.sql')); sqlops.tasks.registerTask('sp_whoisactive.findBlockLeaders', e => this.onExecute(e, 'findBlockLeaders.sql'));
sqlops.tasks.registerTask('sp_whoisactive.getPlans', e => this.onExecute(e, 'getPlans.sql')); sqlops.tasks.registerTask('sp_whoisactive.getPlans', e => this.onExecute(e, 'getPlans.sql'));
return Promise.resolve(true); return Promise.resolve(true);
} }
private onInstall(connection: sqlops.IConnectionProfile): void { private openurl(link: string): void {
openurl.open('http://whoisactive.com/downloads/'); openurl.open(link);
} }
private onExecute(connection: sqlops.IConnectionProfile, fileName: string): void { private onExecute(connection: sqlops.IConnectionProfile, fileName: string): void {

View File

@@ -0,0 +1,4 @@
SELECT blocking_session_id
FROM sys.dm_os_waiting_tasks
WHERE
blocking_session_id IS NOT NULL

View File

@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Directive, Inject, forwardRef, ElementRef } from '@angular/core';
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { getContentHeight, addDisposableListener, EventType } from 'vs/base/browser/dom';
import { AngularDisposable } from 'sql/base/common/lifecycle';
@Directive({
selector: '[scrollable]'
})
export class ScrollableDirective extends AngularDisposable {
private scrollableElement: ScrollableElement;
private parent: HTMLElement;
private scrolled: HTMLElement;
constructor(
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef
) {
super();
this.scrolled = this._el.nativeElement as HTMLElement;
this.parent = this.scrolled.parentElement;
this.parent.removeChild(this.scrolled);
this.scrolled.style.position = 'relative';
this.scrollableElement = new ScrollableElement(this.scrolled, {
horizontal: ScrollbarVisibility.Hidden,
vertical: ScrollbarVisibility.Auto,
useShadows: false
});
this.scrollableElement.onScroll(e => {
this.scrolled.style.bottom = e.scrollTop + 'px';
});
this.parent.appendChild(this.scrollableElement.getDomNode());
const initialHeight = getContentHeight(this.scrolled);
this.scrollableElement.setScrollDimensions({
scrollHeight: getContentHeight(this.scrolled),
height: getContentHeight(this.parent)
});
this._register(addDisposableListener(window, EventType.RESIZE, () => {
this.resetScrollDimensions();
}));
// unforunately because of angular rendering behavior we need to do a double check to make sure nothing changed after this point
setTimeout(() => {
let currentheight = getContentHeight(this.scrolled);
if (initialHeight !== currentheight) {
this.scrollableElement.setScrollDimensions({
scrollHeight: currentheight,
height: getContentHeight(this.parent)
});
}
}, 200);
}
private resetScrollDimensions() {
this.scrollableElement.setScrollDimensions({
scrollHeight: getContentHeight(this.scrolled),
height: getContentHeight(this.parent)
});
}
public layout() {
}
}

View File

@@ -1,6 +1,7 @@
// Adopted and converted to typescript from https://github.com/6pac/SlickGrid/blob/master/plugins/slick.rowdetailview.js // Adopted and converted to typescript from https://github.com/6pac/SlickGrid/blob/master/plugins/slick.rowdetailview.js
// heavily modified // heavily modified
import { mixin } from 'vs/base/common/objects'; import { mixin } from 'vs/base/common/objects';
import * as nls from 'vs/nls';
export class RowDetailView { export class RowDetailView {
@@ -277,7 +278,7 @@ export class RowDetailView {
item._parent = parent; item._parent = parent;
item._offset = offset; item._offset = offset;
item.jobId = parent.jobId; item.jobId = parent.jobId;
item.name = parent.message ? parent.message : 'Error'; item.name = parent.message ? parent.message : nls.localize('rowDetailView.loadError','Loading Error...');
return item; return item;
} }

View File

@@ -88,19 +88,6 @@ export function parseNumAsTimeString(value: number): string {
return tempVal > 0 ? rs + '.' + mss : rs; return tempVal > 0 ? rs + '.' + mss : rs;
} }
/**
* Converts <, >, &, ", ', and any characters that are outside \u00A0 to numeric HTML entity values
* like &#123;
* (Adapted from http://stackoverflow.com/a/18750001)
* @param str String to convert
* @return String with characters replaced.
*/
export function htmlEntities(str: string): string {
return typeof (str) === 'string'
? str.replace(/[\u00A0-\u9999<>\&"']/gim, (i) => { return `&#${i.charCodeAt(0)};`; })
: undefined;
}
export function generateUri(connection: IConnectionProfile, purpose?: 'dashboard' | 'insights' | 'connection'): string { export function generateUri(connection: IConnectionProfile, purpose?: 'dashboard' | 'insights' | 'connection'): string {
let prefix = purpose ? uriPrefixes[purpose] : uriPrefixes.default; let prefix = purpose ? uriPrefixes[purpose] : uriPrefixes.default;
let uri = generateUriWithPrefix(connection, prefix); let uri = generateUriWithPrefix(connection, prefix);

View File

@@ -5,33 +5,37 @@
import 'vs/css!./dashboardHomeContainer'; import 'vs/css!./dashboardHomeContainer';
import { Component, forwardRef, Input, ChangeDetectorRef, Inject, ViewChild } from '@angular/core'; import { Component, forwardRef, Input, ChangeDetectorRef, Inject, ViewChild, ContentChild } from '@angular/core';
import { DashboardWidgetContainer } from 'sql/parts/dashboard/containers/dashboardWidgetContainer.component'; import { DashboardWidgetContainer } from 'sql/parts/dashboard/containers/dashboardWidgetContainer.component';
import { DashboardTab } from 'sql/parts/dashboard/common/interfaces'; import { DashboardTab } from 'sql/parts/dashboard/common/interfaces';
import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget'; import { WidgetConfig } from 'sql/parts/dashboard/common/dashboardWidget';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service'; import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { AngularEventType } from '../../../services/angularEventing/angularEventingService'; import { AngularEventType } from 'sql/services/angularEventing/angularEventingService';
import { DashboardWidgetWrapper } from 'sql/parts/dashboard/contents/dashboardWidgetWrapper.component'; import { DashboardWidgetWrapper } from 'sql/parts/dashboard/contents/dashboardWidgetWrapper.component';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { ScrollableDirective } from 'sql/base/browser/ui/scrollable/scrollable.directive';
@Component({ @Component({
selector: 'dashboard-home-container', selector: 'dashboard-home-container',
providers: [{ provide: DashboardTab, useExisting: forwardRef(() => DashboardHomeContainer) }], providers: [{ provide: DashboardTab, useExisting: forwardRef(() => DashboardHomeContainer) }],
template: ` template: `
<div class="fullsize" style="display: flex; flex-direction: column"> <div class="fullsize" style="display: flex; flex-direction: column">
<dashboard-widget-wrapper #propertiesClass *ngIf="properties" [collapsable]="true" [_config]="properties" <div scrollable>
style="padding-left: 10px; padding-right: 10px; display: block; flex: 0" [style.height.px]="_propertiesClass?.collapsed ? '30' : '90'"> <dashboard-widget-wrapper #propertiesClass *ngIf="properties" [collapsable]="true" [_config]="properties"
</dashboard-widget-wrapper> style="padding-left: 10px; padding-right: 10px; display: block; flex: 0" [style.height.px]="_propertiesClass?.collapsed ? '30' : '90'">
<widget-content style="flex: 1" [widgets]="widgets" [originalConfig]="tab.originalConfig" [context]="tab.context"> </dashboard-widget-wrapper>
</widget-content> <widget-content style="flex: 1" [scrollContent]="false" [widgets]="widgets" [originalConfig]="tab.originalConfig" [context]="tab.context">
</widget-content>
</div>
</div> </div>
` `
}) })
export class DashboardHomeContainer extends DashboardWidgetContainer { export class DashboardHomeContainer extends DashboardWidgetContainer {
@Input() private properties: WidgetConfig; @Input() private properties: WidgetConfig;
@ViewChild('propertiesClass') private _propertiesClass: DashboardWidgetWrapper; @ViewChild('propertiesClass') private _propertiesClass: DashboardWidgetWrapper;
@ContentChild(ScrollableDirective) private _scrollable;
constructor( constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) _cd: ChangeDetectorRef, @Inject(forwardRef(() => ChangeDetectorRef)) _cd: ChangeDetectorRef,
@@ -56,4 +60,9 @@ export class DashboardHomeContainer extends DashboardWidgetContainer {
} }
}); });
} }
public layout() {
super.layout();
this._scrollable.layout();
}
} }

View File

@@ -75,6 +75,7 @@ export class WidgetContent extends AngularDisposable implements AfterViewInit {
@Input() private widgets: WidgetConfig[]; @Input() private widgets: WidgetConfig[];
@Input() private originalConfig: WidgetConfig[]; @Input() private originalConfig: WidgetConfig[];
@Input() private context: string; @Input() private context: string;
@Input() private scrollContent = true;
private _scrollableElement: ScrollableElement; private _scrollableElement: ScrollableElement;
@@ -123,41 +124,43 @@ export class WidgetContent extends AngularDisposable implements AfterViewInit {
} }
ngAfterViewInit() { ngAfterViewInit() {
let container = this._scrollContainer.nativeElement as HTMLElement; if (this.scrollContent) {
let scrollable = this._scrollable.nativeElement as HTMLElement; let container = this._scrollContainer.nativeElement as HTMLElement;
container.removeChild(scrollable); let scrollable = this._scrollable.nativeElement as HTMLElement;
container.removeChild(scrollable);
this._scrollableElement = new ScrollableElement(scrollable, { this._scrollableElement = new ScrollableElement(scrollable, {
horizontal: ScrollbarVisibility.Hidden, horizontal: ScrollbarVisibility.Hidden,
vertical: ScrollbarVisibility.Auto, vertical: ScrollbarVisibility.Auto,
useShadows: false useShadows: false
}); });
this._scrollableElement.onScroll(e => { this._scrollableElement.onScroll(e => {
scrollable.style.bottom = e.scrollTop + 'px'; scrollable.style.bottom = e.scrollTop + 'px';
}); });
container.appendChild(this._scrollableElement.getDomNode()); container.appendChild(this._scrollableElement.getDomNode());
let initalHeight = getContentHeight(scrollable); let initalHeight = getContentHeight(scrollable);
this._scrollableElement.setScrollDimensions({ this._scrollableElement.setScrollDimensions({
scrollHeight: getContentHeight(scrollable), scrollHeight: getContentHeight(scrollable),
height: getContentHeight(container) height: getContentHeight(container)
}); });
this._register(addDisposableListener(window, EventType.RESIZE, () => { this._register(addDisposableListener(window, EventType.RESIZE, () => {
this.resetScrollDimensions(); this.resetScrollDimensions();
})); }));
// unforunately because of angular rendering behavior we need to do a double check to make sure nothing changed after this point // unforunately because of angular rendering behavior we need to do a double check to make sure nothing changed after this point
setTimeout(() => { setTimeout(() => {
let currentheight = getContentHeight(scrollable); let currentheight = getContentHeight(scrollable);
if (initalHeight !== currentheight) { if (initalHeight !== currentheight) {
this._scrollableElement.setScrollDimensions({ this._scrollableElement.setScrollDimensions({
scrollHeight: currentheight, scrollHeight: currentheight,
height: getContentHeight(container) height: getContentHeight(container)
}); });
} }
}, 200); }, 200);
}
} }
public layout() { public layout() {
@@ -167,7 +170,9 @@ export class WidgetContent extends AngularDisposable implements AfterViewInit {
}); });
} }
this._grid.triggerResize(); this._grid.triggerResize();
this.resetScrollDimensions(); if (this.scrollContent) {
this.resetScrollDimensions();
}
} }
private resetScrollDimensions() { private resetScrollDimensions() {

View File

@@ -54,9 +54,10 @@ import { AgentViewComponent } from 'sql/parts/jobManagement/agent/agentView.comp
import { JobHistoryComponent } from 'sql/parts/jobManagement/views/jobHistory.component'; import { JobHistoryComponent } from 'sql/parts/jobManagement/views/jobHistory.component';
let baseComponents = [DashboardHomeContainer, DashboardComponent, DashboardWidgetWrapper, DashboardWebviewContainer, let baseComponents = [DashboardHomeContainer, DashboardComponent, DashboardWidgetWrapper, DashboardWebviewContainer,
DashboardWidgetContainer, DashboardGridContainer, DashboardErrorContainer, DashboardNavSection, ModelViewContent, WebviewContent, WidgetContent, DashboardWidgetContainer, DashboardGridContainer, DashboardErrorContainer, DashboardNavSection, ModelViewContent, WebviewContent, WidgetContent,
ComponentHostDirective, BreadcrumbComponent, ControlHostContent, DashboardControlHostContainer, ComponentHostDirective, BreadcrumbComponent, ControlHostContent, DashboardControlHostContainer,
JobsViewComponent, AgentViewComponent, JobHistoryComponent, JobStepsViewComponent, DashboardModelViewContainer, ModelComponentWrapper]; JobsViewComponent, AgentViewComponent, JobHistoryComponent, JobStepsViewComponent, DashboardModelViewContainer, ModelComponentWrapper,
ScrollableDirective];
/* Panel */ /* Panel */
import { PanelModule } from 'sql/base/browser/ui/panel/panel.module'; import { PanelModule } from 'sql/base/browser/ui/panel/panel.module';
@@ -74,6 +75,7 @@ import { TasksWidget } from 'sql/parts/dashboard/widgets/tasks/tasksWidget.compo
import { InsightsWidget } from 'sql/parts/dashboard/widgets/insights/insightsWidget.component'; import { InsightsWidget } from 'sql/parts/dashboard/widgets/insights/insightsWidget.component';
import { WebviewWidget } from 'sql/parts/dashboard/widgets/webview/webviewWidget.component'; import { WebviewWidget } from 'sql/parts/dashboard/widgets/webview/webviewWidget.component';
import { JobStepsViewComponent } from '../jobManagement/views/jobStepsView.component'; import { JobStepsViewComponent } from '../jobManagement/views/jobStepsView.component';
import { ScrollableDirective } from 'sql/base/browser/ui/scrollable/scrollable.directive';
let widgetComponents = [ let widgetComponents = [
PropertiesWidgetComponent, PropertiesWidgetComponent,

View File

@@ -3,14 +3,14 @@
* 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 * as Utils from 'sql/parts/connection/common/utils'; import * as Strings from 'vs/base/common/strings';
export class DBCellValue { export class DBCellValue {
displayValue: string; displayValue: string;
isNull: boolean; isNull: boolean;
public static isDBCellValue(object: any): boolean { public static isDBCellValue(object: any): boolean {
return (object !== undefined && object.displayValue !== undefined && object.isNull !== undefined); return (object !== undefined && object.displayValue !== undefined && object.isNull !== undefined);
} }
} }
@@ -25,7 +25,7 @@ export function hyperLinkFormatter(row: number, cell: any, value: any, columnDef
valueToDisplay = 'NULL'; valueToDisplay = 'NULL';
if (!value.isNull) { if (!value.isNull) {
cellClasses += ' xmlLink'; cellClasses += ' xmlLink';
valueToDisplay = Utils.htmlEntities(value.displayValue); valueToDisplay = Strings.escape(value.displayValue);
return `<a class="${cellClasses}" href="#" >${valueToDisplay}</a>`; return `<a class="${cellClasses}" href="#" >${valueToDisplay}</a>`;
} else { } else {
cellClasses += ' missing-value'; cellClasses += ' missing-value';
@@ -44,13 +44,12 @@ export function textFormatter(row: number, cell: any, value: any, columnDef: any
if (DBCellValue.isDBCellValue(value)) { if (DBCellValue.isDBCellValue(value)) {
valueToDisplay = 'NULL'; valueToDisplay = 'NULL';
if (!value.isNull) { if (!value.isNull) {
valueToDisplay = Utils.htmlEntities(value.displayValue.replace(/(\r\n|\n|\r)/g, ' ')); valueToDisplay = Strings.escape(value.displayValue.replace(/(\r\n|\n|\r)/g, ' '));
} else { } else {
cellClasses += ' missing-value'; cellClasses += ' missing-value';
} }
} else if (typeof value === 'string'){ } else if (typeof value === 'string') {
valueToDisplay = value; valueToDisplay = Strings.escape(value);
} }
return `<span title="${valueToDisplay}" class="${cellClasses}">${valueToDisplay}</span>`; return `<span title="${valueToDisplay}" class="${cellClasses}">${valueToDisplay}</span>`;

View File

@@ -9,6 +9,9 @@ import * as nls from 'vs/nls';
export class AgentJobUtilities { export class AgentJobUtilities {
public static startIconClass: string = 'icon-start';
public static stopIconClass: string = 'icon-stop';
public static convertToStatusString(status: number): string { public static convertToStatusString(status: number): string {
switch(status) { switch(status) {
case(0): return nls.localize('agentUtilities.failed','Failed'); case(0): return nls.localize('agentUtilities.failed','Failed');
@@ -51,4 +54,41 @@ export class AgentJobUtilities {
return date; return date;
} }
} }
public static setRunnable(icon: HTMLElement, index: number) {
if (icon.className.includes('non-runnable')) {
icon.className = icon.className.slice(0, index);
}
}
public static getActionIconClassName(startIcon: HTMLElement, stopIcon: HTMLElement, executionStatus: number) {
this.setRunnable(startIcon, AgentJobUtilities.startIconClass.length);
this.setRunnable(stopIcon, AgentJobUtilities.stopIconClass.length);
switch (executionStatus) {
case(1): // executing
startIcon.className += ' non-runnable';
return;
case(2): // Waiting for thread
startIcon.className += ' non-runnable';
return;
case(3): // Between retries
startIcon.className += ' non-runnable';
return;
case(4): //Idle
stopIcon.className += ' non-runnable';
return;
case(5): // Suspended
stopIcon.className += ' non-runnable';
return;
case(6): //obsolete
startIcon.className += ' non-runnable';
stopIcon.className += ' non-runnable';
return;
case(7): //Performing Completion Actions
startIcon.className += ' non-runnable';
return;
default:
return;
}
}
} }

View File

@@ -22,8 +22,9 @@ jobhistory-component {
} }
.vs-dark .job-heading-container { .vs-dark .job-heading-container {
height: 32px; height: 49px;
border-bottom: 3px solid #444444; border-bottom: 3px solid #444444;
display: -webkit-box;
} }
#jobsDiv .jobview-grid { #jobsDiv .jobview-grid {
@@ -182,4 +183,11 @@ jobsview-component .jobview-grid > .monaco-table .slick-viewport > .grid-canvas
.vs-dark .jobview-grid > .monaco-table .slick-header-columns .slick-resizable-handle { .vs-dark .jobview-grid > .monaco-table .slick-header-columns .slick-resizable-handle {
border-left: 1px dotted white; border-left: 1px dotted white;
}
.job-heading-container > .icon.in-progress {
height: 20px;
width: 20px;
padding-top: 16px;
padding-left: 15px;
} }

View File

@@ -4,8 +4,10 @@
* 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.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
--> -->
<div class="jobhistory-heading-container">
<h1 class="job-heading">Jobs | {{this._agentJobInfo?.name}} </h1> <h1 class="job-heading">Jobs | {{this._agentJobInfo?.name}} </h1>
<div class="icon in-progress" *ngIf="showProgressWheel()"></div>
</div>
<!-- Back --> <!-- Back -->
<div class="all-jobs"> <div class="all-jobs">

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import 'vs/css!./jobHistory'; import 'vs/css!./jobHistory';
import 'vs/css!sql/media/icons/common-icons';
import { OnInit, OnChanges, Component, Inject, Input, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, ChangeDetectionStrategy, Injectable } from '@angular/core'; import { OnInit, OnChanges, Component, Inject, Input, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, ChangeDetectionStrategy, Injectable } from '@angular/core';
import { AgentJobHistoryInfo, AgentJobInfo } from 'sqlops'; import { AgentJobHistoryInfo, AgentJobInfo } from 'sqlops';
import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IThemeService } from 'vs/platform/theme/common/themeService';
@@ -60,6 +61,7 @@ export class JobHistoryComponent extends Disposable implements OnInit {
private _jobCacheObject: JobCacheObject; private _jobCacheObject: JobCacheObject;
private _notificationService: INotificationService; private _notificationService: INotificationService;
private _agentJobInfo: AgentJobInfo; private _agentJobInfo: AgentJobInfo;
private _noJobsAvailable: boolean = false;
constructor( constructor(
@Inject(BOOTSTRAP_SERVICE_ID) private bootstrapService: IBootstrapService, @Inject(BOOTSTRAP_SERVICE_ID) private bootstrapService: IBootstrapService,
@@ -85,9 +87,6 @@ export class JobHistoryComponent extends Disposable implements OnInit {
this._jobCacheObject.serverName = serverName; this._jobCacheObject.serverName = serverName;
this._jobManagementService.addToCache(serverName, this._jobCacheObject); this._jobManagementService.addToCache(serverName, this._jobCacheObject);
} }
$('#accordion').keypress(e => {
let meme = e;
});
} }
ngOnInit() { ngOnInit() {
@@ -134,6 +133,7 @@ export class JobHistoryComponent extends Disposable implements OnInit {
this._agentJobInfo = this._agentViewComponent.agentJobInfo; this._agentJobInfo = this._agentViewComponent.agentJobInfo;
if (!this.agentJobInfo) { if (!this.agentJobInfo) {
this.agentJobInfo = this._agentJobInfo; this.agentJobInfo = this._agentJobInfo;
this.setActions();
} }
if (this._isVisible === false && this._tableContainer.nativeElement.offsetParent !== null) { if (this._isVisible === false && this._tableContainer.nativeElement.offsetParent !== null) {
this._isVisible = true; this._isVisible = true;
@@ -148,7 +148,10 @@ export class JobHistoryComponent extends Disposable implements OnInit {
} else if (jobHistories && jobHistories.length === 0 ){ } else if (jobHistories && jobHistories.length === 0 ){
this._showPreviousRuns = false; this._showPreviousRuns = false;
this._showSteps = false; this._showSteps = false;
this._noJobsAvailable = true;
this._cd.detectChanges(); this._cd.detectChanges();
} else {
this.loadHistory();
} }
this._jobCacheObject.prevJobID = this._agentViewComponent.jobId; this._jobCacheObject.prevJobID = this._agentViewComponent.jobId;
} else if (this._isVisible === true && this._agentViewComponent.refresh) { } else if (this._isVisible === true && this._agentViewComponent.refresh) {
@@ -276,6 +279,16 @@ export class JobHistoryComponent extends Disposable implements OnInit {
return time.replace('T', ' '); return time.replace('T', ' ');
} }
private showProgressWheel(): boolean {
return this._showPreviousRuns !== true && this._noJobsAvailable === false;
}
private setActions(): void {
let startIcon: HTMLElement = $('.icon-start').get(0);
let stopIcon: HTMLElement = $('.icon-stop').get(0);
AgentJobUtilities.getActionIconClassName(startIcon, stopIcon, this.agentJobInfo.currentExecutionStatus);
}
public get showSteps(): boolean { public get showSteps(): boolean {
return this._showSteps; return this._showSteps;
} }

View File

@@ -29,7 +29,6 @@ ul.action-buttons li {
padding-right: 25px; padding-right: 25px;
display: inline-block; display: inline-block;
width: 50px; width: 50px;
cursor: pointer;
} }
.overview-container .overview-tab .resultsViewCollapsible { .overview-container .overview-tab .resultsViewCollapsible {
@@ -134,6 +133,7 @@ input#accordion:checked ~ .accordion-content {
width: 20px; width: 20px;
background-image: url('../common/media/start.svg'); background-image: url('../common/media/start.svg');
background-repeat: no-repeat; background-repeat: no-repeat;
cursor: pointer;
} }
.vs ul.action-buttons .icon-stop, .vs ul.action-buttons .icon-stop,
@@ -144,6 +144,17 @@ input#accordion:checked ~ .accordion-content {
background-repeat: no-repeat; background-repeat: no-repeat;
height: 20px; height: 20px;
width: 20px; width: 20px;
cursor: pointer;
}
ul.action-buttons div.icon-start.non-runnable {
opacity: 0.4;
cursor: default;
}
ul.action-buttons div.icon-stop.non-runnable {
opacity: 0.4;
cursor: default;
} }
.accordion-content #col1, .accordion-content #col1,
@@ -240,4 +251,15 @@ table.step-list tr.step-row td {
jobhistory-component .history-details .step-table.prev-run-list .monaco-scrollable-element { jobhistory-component .history-details .step-table.prev-run-list .monaco-scrollable-element {
overflow-y: scroll !important; overflow-y: scroll !important;
}
jobhistory-component .jobhistory-heading-container {
display: -webkit-box;
}
jobhistory-component > .jobhistory-heading-container > .icon.in-progress {
width: 20px;
height: 20px;
padding-top: 16px;
padding-left: 20px;
} }

View File

@@ -7,6 +7,7 @@
<div class="job-heading-container"> <div class="job-heading-container">
<h1 class="job-heading" *ngIf="_isCloud === false">Jobs</h1> <h1 class="job-heading" *ngIf="_isCloud === false">Jobs</h1>
<h1 class="job-heading" *ngIf="_isCloud === true">No Jobs Available</h1> <h1 class="job-heading" *ngIf="_isCloud === true">No Jobs Available</h1>
<div class="icon in-progress" *ngIf="_showProgressWheel === true"></div>
</div> </div>
<div #jobsgrid class="jobview-grid"></div> <div #jobsgrid class="jobview-grid"></div>

View File

@@ -9,6 +9,7 @@ import 'vs/css!sql/parts/grid/media/styles';
import 'vs/css!sql/parts/grid/media/slick.grid'; import 'vs/css!sql/parts/grid/media/slick.grid';
import 'vs/css!sql/parts/grid/media/slickGrid'; import 'vs/css!sql/parts/grid/media/slickGrid';
import 'vs/css!../common/media/jobs'; import 'vs/css!../common/media/jobs';
import 'vs/css!sql/media/icons/common-icons';
import { Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, AfterContentChecked } from '@angular/core'; import { Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, AfterContentChecked } from '@angular/core';
import * as Utils from 'sql/parts/connection/common/utils'; import * as Utils from 'sql/parts/connection/common/utils';
@@ -72,6 +73,7 @@ export class JobsViewComponent implements AfterContentChecked {
public jobHistories: { [jobId: string]: sqlops.AgentJobHistoryInfo[]; } = Object.create(null); public jobHistories: { [jobId: string]: sqlops.AgentJobHistoryInfo[]; } = Object.create(null);
private _serverName: string; private _serverName: string;
private _isCloud: boolean; private _isCloud: boolean;
private _showProgressWheel: boolean;
constructor( constructor(
@Inject(BOOTSTRAP_SERVICE_ID) private bootstrapService: IBootstrapService, @Inject(BOOTSTRAP_SERVICE_ID) private bootstrapService: IBootstrapService,
@@ -99,18 +101,22 @@ export class JobsViewComponent implements AfterContentChecked {
this.isVisible = true; this.isVisible = true;
if (!this.isInitialized) { if (!this.isInitialized) {
if (this._jobCacheObject.serverName === this._serverName && this._jobCacheObject.jobs.length > 0) { if (this._jobCacheObject.serverName === this._serverName && this._jobCacheObject.jobs.length > 0) {
this._showProgressWheel = true;
this.jobs = this._jobCacheObject.jobs; this.jobs = this._jobCacheObject.jobs;
this.onFirstVisible(true); this.onFirstVisible(true);
this.isInitialized = true; this.isInitialized = true;
} else { } else {
this._showProgressWheel = true;
this.onFirstVisible(false); this.onFirstVisible(false);
this.isInitialized = true; this.isInitialized = true;
} }
} }
} else if (this.isVisible === true && this._agentViewComponent.refresh === true) { } else if (this.isVisible === true && this._agentViewComponent.refresh === true) {
this._showProgressWheel = true;
this.onFirstVisible(false); this.onFirstVisible(false);
this._agentViewComponent.refresh = false; this._agentViewComponent.refresh = false;
} else if (this.isVisible === true && this._agentViewComponent.refresh === false) { } else if (this.isVisible === true && this._agentViewComponent.refresh === false) {
this._showProgressWheel = true;
this.onFirstVisible(true); this.onFirstVisible(true);
} else if (this.isVisible === true && this._gridEl.nativeElement.offsetParent === null) { } else if (this.isVisible === true && this._gridEl.nativeElement.offsetParent === null) {
this.isVisible = false; this.isVisible = false;
@@ -207,7 +213,7 @@ export class JobsViewComponent implements AfterContentChecked {
if (job.lastRunOutcome === 0 && !expandedJobs.get(job.jobId)) { if (job.lastRunOutcome === 0 && !expandedJobs.get(job.jobId)) {
this.expandJobRowDetails(i+expandedJobs.size); this.expandJobRowDetails(i+expandedJobs.size);
this.addToStyleHash(i+expandedJobs.size); this.addToStyleHash(i+expandedJobs.size);
this._agentViewComponent.setExpanded(job.jobId, 'temp'); this._agentViewComponent.setExpanded(job.jobId, 'Loading Error...');
} else if (job.lastRunOutcome === 0 && expandedJobs.get(job.jobId)) { } else if (job.lastRunOutcome === 0 && expandedJobs.get(job.jobId)) {
this.expandJobRowDetails(i+expansions); this.expandJobRowDetails(i+expansions);
this.addToStyleHash(i+expansions); this.addToStyleHash(i+expansions);
@@ -219,6 +225,8 @@ export class JobsViewComponent implements AfterContentChecked {
let currentTarget = e.currentTarget; let currentTarget = e.currentTarget;
currentTarget.title = currentTarget.innerText; currentTarget.title = currentTarget.innerText;
}); });
this._showProgressWheel = false;
this._cd.detectChanges();
this.loadJobHistories(); this.loadJobHistories();
} }
@@ -298,7 +306,7 @@ export class JobsViewComponent implements AfterContentChecked {
let item = self.dataView.getItemById(job.jobId + '.error'); let item = self.dataView.getItemById(job.jobId + '.error');
let noStepsMessage = nls.localize('jobsView.noSteps', 'No Steps available for this job.'); let noStepsMessage = nls.localize('jobsView.noSteps', 'No Steps available for this job.');
let errorMessage = jobHistory ? jobHistory.message: noStepsMessage; let errorMessage = jobHistory ? jobHistory.message: noStepsMessage;
item['name'] = item['name'] + ': ' + errorMessage; item['name'] = nls.localize('jobsView.error', 'Error: ') + errorMessage;
self._agentViewComponent.setExpanded(job.jobId, errorMessage); self._agentViewComponent.setExpanded(job.jobId, errorMessage);
self.dataView.updateItem(job.jobId + '.error', item); self.dataView.updateItem(job.jobId + '.error', item);

View File

@@ -0,0 +1,97 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ComponentFactoryResolver,
ViewChild, ViewChildren, ElementRef, Injector, OnDestroy, QueryList, AfterViewInit
} from '@angular/core';
import * as sqlops from 'sqlops';
import Event, { Emitter } from 'vs/base/common/event';
import { ComponentBase } from 'sql/parts/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { attachButtonStyler } from 'sql/common/theme/styler';
import { Button } from 'sql/base/browser/ui/button/button';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
@Component({
selector: 'button',
template: `
<div #input style="width: 100%"></div>
`
})
export default class ButtonComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _button: Button;
@ViewChild('input', { read: ElementRef }) private _inputContainer: ElementRef;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef) {
super(changeRef);
}
ngOnInit(): void {
this.baseInit();
}
ngAfterViewInit(): void {
if (this._inputContainer) {
this._button = new Button(this._inputContainer.nativeElement);
this._register(this._button);
this._register(attachButtonStyler(this._button, this._commonService.themeService, {
buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND
}));
this._register(this._button.onDidClick(e => {
this._onEventEmitter.fire({
eventType: ComponentEventType.onDidClick,
args: e
});
}));
}
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public layout(): void {
this._changeRef.detectChanges();
}
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
this._button.label = this.label;
}
// CSS-bound properties
private get label(): string {
return this.getPropertyOrDefault<sqlops.ButtonProperties, string>((props) => props.label, '');
}
private set label(newValue: string) {
this.setPropertyFromUI<sqlops.ButtonProperties, string>(this.setValueProperties, newValue);
}
private setValueProperties(properties: sqlops.ButtonProperties, label: string): void {
properties.label = label;
}
}

View File

@@ -75,7 +75,7 @@ export abstract class ComponentBase extends Disposable implements IComponent, On
return types.isUndefinedOrNull(property) ? defaultVal : property; return types.isUndefinedOrNull(property) ? defaultVal : property;
} }
protected setProperty<TPropertyBag, TValue>(propertySetter: (TPropertyBag, TValue) => void, value: TValue) { protected setPropertyFromUI<TPropertyBag, TValue>(propertySetter: (TPropertyBag, TValue) => void, value: TValue) {
propertySetter(this.getProperties<TPropertyBag>(), value); propertySetter(this.getProperties<TPropertyBag>(), value);
this._onEventEmitter.fire({ this._onEventEmitter.fire({
eventType: ComponentEventType.PropertiesChanged, eventType: ComponentEventType.PropertiesChanged,
@@ -86,6 +86,12 @@ export abstract class ComponentBase extends Disposable implements IComponent, On
public get onEvent(): Event<IComponentEventArgs> { public get onEvent(): Event<IComponentEventArgs> {
return this._onEventEmitter.event; return this._onEventEmitter.event;
} }
public get title(): string {
let properties = this.getProperties();
let title = properties['title'];
return title ? <string>title : '';
}
} }
export abstract class ContainerBase<T> extends ComponentBase { export abstract class ContainerBase<T> extends ComponentBase {

View File

@@ -4,16 +4,28 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import FlexContainer from './flexContainer.component'; import FlexContainer from './flexContainer.component';
import FormContainer from './formContainer.component';
import CardComponent from './card.component'; import CardComponent from './card.component';
import InputBoxComponent from './inputbox.component'; import InputBoxComponent from './inputbox.component';
import DropDownComponent from './dropdown.component';
import ButtonComponent from './button.component';
import { registerComponentType } from 'sql/platform/dashboard/common/modelComponentRegistry'; import { registerComponentType } from 'sql/platform/dashboard/common/modelComponentRegistry';
import { ModelComponentTypes } from 'sql/workbench/api/common/sqlExtHostTypes'; import { ModelComponentTypes } from 'sql/workbench/api/common/sqlExtHostTypes';
export const FLEX_CONTAINER = 'flex-container'; export const FLEX_CONTAINER = 'flex-container';
registerComponentType(FLEX_CONTAINER, ModelComponentTypes.FlexContainer, FlexContainer); registerComponentType(FLEX_CONTAINER, ModelComponentTypes.FlexContainer, FlexContainer);
export const FORM_CONTAINER = 'form-container';
registerComponentType(FORM_CONTAINER, ModelComponentTypes.Form, FormContainer);
export const CARD_COMPONENT = 'card-component'; export const CARD_COMPONENT = 'card-component';
registerComponentType(CARD_COMPONENT, ModelComponentTypes.Card, CardComponent); registerComponentType(CARD_COMPONENT, ModelComponentTypes.Card, CardComponent);
export const INPUTBOX_COMPONENT = 'inputbox-component'; export const INPUTBOX_COMPONENT = 'inputbox-component';
registerComponentType(INPUTBOX_COMPONENT, ModelComponentTypes.InputBox, InputBoxComponent); registerComponentType(INPUTBOX_COMPONENT, ModelComponentTypes.InputBox, InputBoxComponent);
export const DROPDOWN_COMPONENT = 'dropdown-component';
registerComponentType(DROPDOWN_COMPONENT, ModelComponentTypes.DropDown, DropDownComponent);
export const BUTTON_COMPONENT = 'button-component';
registerComponentType(BUTTON_COMPONENT, ModelComponentTypes.Button, ButtonComponent);

View File

@@ -0,0 +1,117 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ComponentFactoryResolver,
ViewChild, ViewChildren, ElementRef, Injector, OnDestroy, QueryList, AfterViewInit
} from '@angular/core';
import * as sqlops from 'sqlops';
import Event, { Emitter } from 'vs/base/common/event';
import { ComponentBase } from 'sql/parts/modelComponents/componentBase';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces';
import { Dropdown, IDropdownOptions } from 'sql/base/browser/ui/editableDropdown/dropdown';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { attachListStyler } from 'vs/platform/theme/common/styler';
import { attachEditableDropdownStyler } from 'sql/common/theme/styler';
@Component({
selector: 'inputBox',
template: `
<div #input style="width: 100%"></div>
`
})
export default class DropDownComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _dropdown: Dropdown;
@ViewChild('input', { read: ElementRef }) private _inputContainer: ElementRef;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef) {
super(changeRef);
}
ngOnInit(): void {
this.baseInit();
}
ngAfterViewInit(): void {
if (this._inputContainer) {
let dropdownOptions: IDropdownOptions = {
values: [],
strictSelection: false,
placeholder: '',
maxHeight: 125,
ariaLabel: ''
};
this._dropdown = new Dropdown(this._inputContainer.nativeElement, this._commonService.contextViewService, this._commonService.themeService,
dropdownOptions);
this._register(this._dropdown);
this._register(attachEditableDropdownStyler(this._dropdown, this._commonService.themeService));
this._register(this._dropdown.onValueChange(e => {
this.value = this._dropdown.value;
this._onEventEmitter.fire({
eventType: ComponentEventType.onDidChange,
args: e
});
}));
}
}
ngOnDestroy(): void {
this.baseDestroy();
}
/// IComponent implementation
public layout(): void {
this._changeRef.detectChanges();
}
public setLayout(layout: any): void {
// TODO allow configuring the look and feel
this.layout();
}
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
this._dropdown.values = this.values ? this.values : [];
if (this.value) {
this._dropdown.value = this.value;
}
}
// CSS-bound properties
private get value(): string {
return this.getPropertyOrDefault<sqlops.DropDownProperties, string>((props) => props.value, '');
}
private set value(newValue: string) {
this.setPropertyFromUI<sqlops.DropDownProperties, string>(this.setValueProperties, newValue);
}
private get values(): string[] {
return this.getPropertyOrDefault<sqlops.DropDownProperties, string[]>((props) => props.values, undefined);
}
private set values(newValue: string[]) {
this.setPropertyFromUI<sqlops.DropDownProperties, string[]>(this.setValuesProperties, newValue);
}
private setValueProperties(properties: sqlops.DropDownProperties, value: string): void {
properties.value = value;
}
private setValuesProperties(properties: sqlops.DropDownProperties, values: string[]): void {
properties.values = values;
}
}

View File

@@ -0,0 +1,128 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./formLayout';
import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ComponentFactoryResolver,
ViewChild, ViewChildren, ElementRef, Injector, OnDestroy, QueryList, AfterViewInit
} from '@angular/core';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces';
import { FormLayout, FormItemLayout } from 'sqlops';
import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service';
import { ContainerBase } from 'sql/parts/modelComponents/componentBase';
import { ModelComponentWrapper } from 'sql/parts/modelComponents/modelComponentWrapper.component';
import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
export interface TitledFormItemLayout {
title: string;
actions?: string[];
isFormComponent: Boolean;
}
class FormItem {
constructor(public descriptor: IComponentDescriptor, public config: TitledFormItemLayout) { }
}
@Component({
template: `
<div #container *ngIf="items" class="form-table"
[style.alignItems]="alignItems" [style.alignContent]="alignContent">
<div *ngFor="let item of items" class="form-row">
<ng-container *ngIf="isFormComponent(item)">
<div class="form-cell">{{getItemTitle(item)}}</div>
<div class="form-cell">
<model-component-wrapper [descriptor]="item.descriptor" [modelStore]="modelStore">
</model-component-wrapper>
</div>
<div *ngIf="itemHasActions(item)" class="form-cell">
<div *ngFor="let actionItem of getActionComponents(item)" >
<model-component-wrapper [descriptor]="actionItem.descriptor" [modelStore]="modelStore">
</model-component-wrapper>
</div>
</div>
</ng-container>
</div>
</div>
`
})
export default class FormContainer extends ContainerBase<FormItemLayout> implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _alignItems: string;
private _alignContent: string;
@ViewChildren(ModelComponentWrapper) private _componentWrappers: QueryList<ModelComponentWrapper>;
@ViewChild('container', { read: ElementRef }) private _container: ElementRef;
constructor (
@Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef) {
super(changeRef);
}
ngOnInit(): void {
this.baseInit();
}
ngOnDestroy(): void {
this.baseDestroy();
}
ngAfterViewInit(): void {
}
/// IComponent implementation
public layout(): void {
if (this._componentWrappers) {
this._componentWrappers.forEach(wrapper => {
wrapper.layout();
});
}
}
public get alignItems(): string {
return this._alignItems;
}
public get alignContent(): string {
return this._alignContent;
}
private getItemTitle(item: FormItem): string {
let itemConfig = item.config;
return itemConfig ? itemConfig.title : '';
}
private getActionComponents(item: FormItem): FormItem[]{
let items = this.items;
let itemConfig = item.config;
if (itemConfig && itemConfig.actions) {
let resultItems = itemConfig.actions.map(x => {
let actionComponent = items.find(i => i.descriptor.id === x);
return <FormItem>actionComponent;
});
return resultItems.filter(r => r && r.descriptor);
}
return [];
}
private isFormComponent(item: FormItem): Boolean {
return item && item.config && item.config.isFormComponent;
}
private itemHasActions(item: FormItem): Boolean {
let itemConfig = item.config;
return itemConfig && itemConfig.actions !== undefined && itemConfig.actions.length > 0;
}
public setLayout(layout: any): void {
this.layout();
}
}

View File

@@ -0,0 +1,20 @@
.form-table {
width:400px;
display:table;
padding: 30px;
}
.form-row {
display: table-row;
width: 100px;
}
.form-cell {
padding: 5px;
display: table-cell;
}
.form-action {
width: 20px;
}

View File

@@ -3,7 +3,8 @@
* 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 { Component, Input, Inject, ChangeDetectorRef, forwardRef, ComponentFactoryResolver, import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, ComponentFactoryResolver,
ViewChild, ViewChildren, ElementRef, Injector, OnDestroy, QueryList, AfterViewInit ViewChild, ViewChildren, ElementRef, Injector, OnDestroy, QueryList, AfterViewInit
} from '@angular/core'; } from '@angular/core';
@@ -70,7 +71,7 @@ export default class InputBoxComponent extends ComponentBase implements ICompone
this._changeRef.detectChanges(); this._changeRef.detectChanges();
} }
public setLayout (layout: any): void { public setLayout(layout: any): void {
// TODO allow configuring the look and feel // TODO allow configuring the look and feel
this.layout(); this.layout();
} }
@@ -87,7 +88,7 @@ export default class InputBoxComponent extends ComponentBase implements ICompone
} }
public set value(newValue: string) { public set value(newValue: string) {
this.setProperty<sqlops.InputBoxProperties, string>(this.setInputBoxProperties, newValue); this.setPropertyFromUI<sqlops.InputBoxProperties, string>(this.setInputBoxProperties, newValue);
} }
private setInputBoxProperties(properties: sqlops.InputBoxProperties, value: string): void { private setInputBoxProperties(properties: sqlops.InputBoxProperties, value: string): void {

View File

@@ -21,6 +21,7 @@ export interface IComponent {
addToContainer?: (componentDescriptor: IComponentDescriptor, config: any) => void; addToContainer?: (componentDescriptor: IComponentDescriptor, config: any) => void;
setLayout?: (layout: any) => void; setLayout?: (layout: any) => void;
setProperties?: (properties: { [key: string]: any; }) => void; setProperties?: (properties: { [key: string]: any; }) => void;
title?: string;
onEvent?: Event<IComponentEventArgs>; onEvent?: Event<IComponentEventArgs>;
} }
@@ -53,11 +54,13 @@ export interface IComponentDescriptor {
export interface IComponentEventArgs { export interface IComponentEventArgs {
eventType: ComponentEventType; eventType: ComponentEventType;
args: any; args: any;
componentId?: string;
} }
export enum ComponentEventType { export enum ComponentEventType {
PropertiesChanged, PropertiesChanged,
onDidChange onDidChange,
onDidClick
} }
export interface IModelStore { export interface IModelStore {

View File

@@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
import nls = require('vs/nls'); import nls = require('vs/nls');
import * as sqlops from 'sqlops'; import * as sqlops from 'sqlops';
import { IModelStore, IComponentDescriptor, IComponent } from './interfaces'; import { IModelStore, IComponentDescriptor, IComponent, IComponentEventArgs } from './interfaces';
import { IItemConfig, ModelComponentTypes, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes'; import { IItemConfig, ModelComponentTypes, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes';
import { IModelView } from 'sql/services/model/modelViewService'; import { IModelView } from 'sql/services/model/modelViewService';
import { Extensions, IComponentRegistry } from 'sql/platform/dashboard/common/modelComponentRegistry'; import { Extensions, IComponentRegistry } from 'sql/platform/dashboard/common/modelComponentRegistry';
@@ -18,7 +18,7 @@ import { AngularDisposable } from 'sql/base/common/lifecycle';
import { ModelStore } from 'sql/parts/modelComponents/modelStore'; import { ModelStore } from 'sql/parts/modelComponents/modelStore';
import Event, { Emitter } from 'vs/base/common/event'; import Event, { Emitter } from 'vs/base/common/event';
const componentRegistry = <IComponentRegistry> Registry.as(Extensions.ComponentContribution); const componentRegistry = <IComponentRegistry>Registry.as(Extensions.ComponentContribution);
/** /**
* Provides common logic required for any implementation that hooks to a model provided by * Provides common logic required for any implementation that hooks to a model provided by
@@ -57,7 +57,7 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
this.setLayout(component.id, component.layout); this.setLayout(component.id, component.layout);
this.registerEvent(component.id); this.registerEvent(component.id);
if (component.itemConfigs) { if (component.itemConfigs) {
for(let item of component.itemConfigs) { for (let item of component.itemConfigs) {
this.addToContainer(component.id, item); this.addToContainer(component.id, item);
} }
} }
@@ -66,12 +66,12 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
} }
clearContainer(componentId: string): void { clearContainer(componentId: string): void {
this.queueAction(componentId, (component) => component.clearContainer()); this.queueAction(componentId, (component) => component.clearContainer());
} }
addToContainer(containerId: string, itemConfig: IItemConfig): void { addToContainer(containerId: string, itemConfig: IItemConfig): void {
// Do not return the promise as this should be non-blocking // Do not return the promise as this should be non-blocking
this.queueAction(containerId, (component) => { this.queueAction(containerId, (component) => {
let childDescriptor = this.defineComponent(itemConfig.componentShape); let childDescriptor = this.defineComponent(itemConfig.componentShape);
component.addToContainer(childDescriptor, itemConfig.config); component.addToContainer(childDescriptor, itemConfig.config);
}); });
@@ -81,14 +81,14 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
if (!layout) { if (!layout) {
return; return;
} }
this.queueAction(componentId, (component) => component.setLayout(layout)); this.queueAction(componentId, (component) => component.setLayout(layout));
} }
setProperties(componentId: string, properties: { [key: string]: any; }): void { setProperties(componentId: string, properties: { [key: string]: any; }): void {
if (!properties) { if (!properties) {
return; return;
} }
this.queueAction(componentId, (component) => component.setProperties(properties)); this.queueAction(componentId, (component) => component.setProperties(properties));
} }
private queueAction<T>(componentId: string, action: (component: IComponent) => T): void { private queueAction<T>(componentId: string, action: (component: IComponent) => T): void {
@@ -98,16 +98,17 @@ export abstract class ViewBase extends AngularDisposable implements IModelView {
} }
registerEvent(componentId: string) { registerEvent(componentId: string) {
this.queueAction(componentId, (component) => { this.queueAction(componentId, (component) => {
if (component.onEvent) { if (component.onEvent) {
this._register(component.onEvent(e => { this._register(component.onEvent(e => {
e.componentId = componentId;
this._onEventEmitter.fire(e); this._onEventEmitter.fire(e);
})); }));
} }
}); });
} }
public get onEvent(): Event<any> { public get onEvent(): Event<IComponentEventArgs> {
return this._onEventEmitter.event; return this._onEventEmitter.event;
} }
} }

View File

@@ -137,7 +137,7 @@ actionRegistry.registerWorkbenchAction(
FocusOnCurrentQueryKeyboardAction, FocusOnCurrentQueryKeyboardAction,
FocusOnCurrentQueryKeyboardAction.ID, FocusOnCurrentQueryKeyboardAction.ID,
FocusOnCurrentQueryKeyboardAction.LABEL, FocusOnCurrentQueryKeyboardAction.LABEL,
{ primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_Q } { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_O }
), ),
FocusOnCurrentQueryKeyboardAction.LABEL FocusOnCurrentQueryKeyboardAction.LABEL
); );

View File

@@ -12,14 +12,24 @@ import { Dialog } from 'sql/platform/dialog/dialogTypes';
import { IModalOptions } from 'sql/base/browser/ui/modal/modal'; import { IModalOptions } from 'sql/base/browser/ui/modal/modal';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
const defaultOptions: IModalOptions = { hasBackButton: true, isWide: true }; const defaultOptions: IModalOptions = { hasBackButton: true, isWide: false };
export class CustomDialogService { export class CustomDialogService {
private _dialogModals = new Map<Dialog, DialogModal>();
constructor( @IInstantiationService private _instantiationService: IInstantiationService) { } constructor( @IInstantiationService private _instantiationService: IInstantiationService) { }
public showDialog(dialog: Dialog, options?: IModalOptions): void { public showDialog(dialog: Dialog, options?: IModalOptions): void {
let optionsDialog = this._instantiationService.createInstance(DialogModal, dialog, 'CustomDialog', options || defaultOptions); let dialogModal = this._instantiationService.createInstance(DialogModal, dialog, 'CustomDialog', options || defaultOptions);
optionsDialog.render(); this._dialogModals.set(dialog, dialogModal);
optionsDialog.open(); dialogModal.render();
dialogModal.open();
}
public closeDialog(dialog: Dialog): void {
let dialogModal = this._dialogModals.get(dialog);
if (dialogModal) {
dialogModal.cancel();
}
} }
} }

View File

@@ -8,7 +8,7 @@
import 'vs/css!./media/dialogModal'; import 'vs/css!./media/dialogModal';
import { Modal, IModalOptions } from 'sql/base/browser/ui/modal/modal'; import { Modal, IModalOptions } from 'sql/base/browser/ui/modal/modal';
import { attachModalDialogStyler } from 'sql/common/theme/styler'; import { attachModalDialogStyler } from 'sql/common/theme/styler';
import { Dialog } from 'sql/platform/dialog/dialogTypes'; import { Dialog, DialogButton } from 'sql/platform/dialog/dialogTypes';
import { DialogPane } from 'sql/platform/dialog/dialogPane'; import { DialogPane } from 'sql/platform/dialog/dialogPane';
import { IBootstrapService } from 'sql/services/bootstrap/bootstrapService'; import { IBootstrapService } from 'sql/services/bootstrap/bootstrapService';
import { Builder } from 'vs/base/browser/builder'; import { Builder } from 'vs/base/browser/builder';
@@ -23,9 +23,6 @@ import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
export class DialogModal extends Modal { export class DialogModal extends Modal {
private static readonly DONE_BUTTON_LABEL = localize('dialogModalDoneButtonLabel', 'Done');
private static readonly CANCEL_BUTTON_LABEL = localize('dialogModalCancelButtonLabel', 'Cancel');
private _dialogPane: DialogPane; private _dialogPane: DialogPane;
// Wizard HTML elements // Wizard HTML elements
@@ -61,10 +58,34 @@ export class DialogModal extends Modal {
attachButtonStyler(this.backButton, this._themeService, { buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND }); attachButtonStyler(this.backButton, this._themeService, { buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND });
} }
this._cancelButton = this.addFooterButton(DialogModal.CANCEL_BUTTON_LABEL, () => this.cancel()); if (this._dialog.customButtons) {
this._doneButton = this.addFooterButton(DialogModal.DONE_BUTTON_LABEL, () => this.done()); this._dialog.customButtons.forEach(button => {
attachButtonStyler(this._cancelButton, this._themeService); let buttonElement = this.addDialogButton(button);
attachButtonStyler(this._doneButton, this._themeService); this.updateButtonElement(buttonElement, button);
});
}
this._cancelButton = this.addDialogButton(this._dialog.cancelButton, () => this.cancel());
this.updateButtonElement(this._cancelButton, this._dialog.cancelButton);
this._doneButton = this.addDialogButton(this._dialog.okButton, () => this.done());
this.updateButtonElement(this._doneButton, this._dialog.okButton);
}
private addDialogButton(button: DialogButton, onSelect: () => void = () => undefined): Button {
let buttonElement = this.addFooterButton(button.label, onSelect);
buttonElement.enabled = button.enabled;
button.registerClickEvent(buttonElement.onDidClick);
button.onUpdate(() => {
this.updateButtonElement(buttonElement, button);
});
attachButtonStyler(buttonElement, this._themeService);
return buttonElement;
}
private updateButtonElement(buttonElement: Button, dialogButton: DialogButton) {
buttonElement.label = dialogButton.label;
buttonElement.enabled = dialogButton.enabled;
dialogButton.hidden ? buttonElement.element.classList.add('dialogModal-hidden') : buttonElement.element.classList.remove('dialogModal-hidden');
} }
protected renderBody(container: HTMLElement): void { protected renderBody(container: HTMLElement): void {

View File

@@ -92,4 +92,9 @@ export class DialogPane extends Disposable implements IThemable {
this._body.style.backgroundColor = styles.dialogBodyBackground ? styles.dialogBodyBackground.toString() : undefined; this._body.style.backgroundColor = styles.dialogBodyBackground ? styles.dialogBodyBackground.toString() : undefined;
this._body.style.color = styles.dialogForeground ? styles.dialogForeground.toString() : undefined; this._body.style.color = styles.dialogForeground ? styles.dialogForeground.toString() : undefined;
} }
public dispose() {
super.dispose();
this._moduleRef.destroy();
}
} }

View File

@@ -6,6 +6,7 @@
'use strict'; 'use strict';
import * as sqlops from 'sqlops'; import * as sqlops from 'sqlops';
import { localize } from 'vs/nls';
import Event, { Emitter } from 'vs/base/common/event'; import Event, { Emitter } from 'vs/base/common/event';
export class DialogTab implements sqlops.window.modelviewdialog.DialogTab { export class DialogTab implements sqlops.window.modelviewdialog.DialogTab {
@@ -16,40 +17,70 @@ export class DialogTab implements sqlops.window.modelviewdialog.DialogTab {
this.content = content; this.content = content;
} }
} }
public updateContent(): void { }
} }
export class Dialog implements sqlops.window.modelviewdialog.Dialog { export class Dialog implements sqlops.window.modelviewdialog.Dialog {
public content: string | DialogTab[]; private static readonly DONE_BUTTON_LABEL = localize('dialogModalDoneButtonLabel', 'Done');
public okTitle: string; private static readonly CANCEL_BUTTON_LABEL = localize('dialogModalCancelButtonLabel', 'Cancel');
public cancelTitle: string;
public customButtons: DialogButton[];
private _onOk: Emitter<void> = new Emitter<void>(); public content: string | DialogTab[];
public readonly onOk: Event<void> = this._onOk.event; public okButton: DialogButton = new DialogButton(Dialog.DONE_BUTTON_LABEL, true);
private _onCancel: Emitter<void> = new Emitter<void>(); public cancelButton: DialogButton = new DialogButton(Dialog.CANCEL_BUTTON_LABEL, true);
public readonly onCancel: Event<void> = this._onCancel.event; public customButtons: DialogButton[];
constructor(public title: string, content?: string | DialogTab[]) { constructor(public title: string, content?: string | DialogTab[]) {
if (content) { if (content) {
this.content = content; this.content = content;
} }
} }
public open(): void { }
public close(): void { }
public updateContent(): void { }
} }
export class DialogButton implements sqlops.window.modelviewdialog.Button { export class DialogButton implements sqlops.window.modelviewdialog.Button {
public label: string; private _label: string;
public enabled: boolean; private _enabled: boolean;
private _hidden: boolean;
private _onClick: Emitter<void> = new Emitter<void>(); private _onClick: Emitter<void> = new Emitter<void>();
public readonly onClick: Event<void> = this._onClick.event; public readonly onClick: Event<void> = this._onClick.event;
private _onUpdate: Emitter<void> = new Emitter<void>();
public readonly onUpdate: Event<void> = this._onUpdate.event;
constructor(label: string, enabled: boolean) { constructor(label: string, enabled: boolean) {
this.label = label; this._label = label;
this.enabled = enabled; this._enabled = enabled;
this._hidden = false;
}
public get label(): string {
return this._label;
}
public set label(label: string) {
this._label = label;
this._onUpdate.fire();
}
public get enabled(): boolean {
return this._enabled;
}
public set enabled(enabled: boolean) {
this._enabled = enabled;
this._onUpdate.fire();
}
public get hidden(): boolean {
return this._hidden;
}
public set hidden(hidden: boolean) {
this._hidden = hidden;
this._onUpdate.fire();
}
/**
* Register an event that notifies the button that it has been clicked
*/
public registerClickEvent(clickEvent: Event<void>): void {
clickEvent(() => this._onClick.fire());
} }
} }

View File

@@ -23,6 +23,6 @@
height: 100%; height: 100%;
} }
.dialogModal-pane.dialogModal-hidden { .dialogModal-hidden {
display: none; display: none;
} }

View File

@@ -20,23 +20,30 @@ declare module 'sqlops' {
flexContainer(): FlexBuilder; flexContainer(): FlexBuilder;
card(): ComponentBuilder<CardComponent>; card(): ComponentBuilder<CardComponent>;
inputBox(): ComponentBuilder<InputBoxComponent>; inputBox(): ComponentBuilder<InputBoxComponent>;
button(): ComponentBuilder<ButtonComponent>;
dropDown(): ComponentBuilder<DropDownComponent>;
dashboardWidget(widgetId: string): ComponentBuilder<WidgetComponent>; dashboardWidget(widgetId: string): ComponentBuilder<WidgetComponent>;
dashboardWebview(webviewId: string): ComponentBuilder<WebviewComponent>; dashboardWebview(webviewId: string): ComponentBuilder<WebviewComponent>;
formContainer(): FormBuilder;
} }
export interface ComponentBuilder<T extends Component> { export interface ComponentBuilder<T extends Component> {
component(): T; component(): T;
withProperties<U>(properties: U): ComponentBuilder<T>; withProperties<U>(properties: U): ComponentBuilder<T>;
} }
export interface ContainerBuilder<T extends Component, TLayout,TItemLayout> extends ComponentBuilder<T> { export interface ContainerBuilder<T extends Component, TLayout, TItemLayout> extends ComponentBuilder<T> {
withLayout(layout: TLayout): ContainerBuilder<T, TLayout, TItemLayout>; withLayout(layout: TLayout): ContainerBuilder<T, TLayout, TItemLayout>;
withItems(components: Array<Component>, itemLayout ?: TItemLayout): ContainerBuilder<T, TLayout, TItemLayout>; withItems(components: Array<Component>, itemLayout?: TItemLayout): ContainerBuilder<T, TLayout, TItemLayout>;
} }
export interface FlexBuilder extends ContainerBuilder<FlexContainer, FlexLayout, FlexItemLayout> { export interface FlexBuilder extends ContainerBuilder<FlexContainer, FlexLayout, FlexItemLayout> {
} }
export interface FormBuilder extends ContainerBuilder<FormContainer, FormLayout, FormItemLayout> {
withFormItems(components: FormComponent[], itemLayout?: FormItemLayout): ContainerBuilder<FormContainer, FormLayout, FormItemLayout>;
}
export interface Component { export interface Component {
readonly id: string; readonly id: string;
@@ -50,10 +57,16 @@ declare module 'sqlops' {
updateProperties(properties: { [key: string]: any }): Thenable<boolean>; updateProperties(properties: { [key: string]: any }): Thenable<boolean>;
} }
export interface FormComponent {
component: Component;
title: string;
actions?: Component[];
}
/** /**
* A component that contains other components * A component that contains other components
*/ */
export interface Container<TLayout,TItemLayout> extends Component { export interface Container<TLayout, TItemLayout> extends Component {
/** /**
* A copy of the child items array. This cannot be added to directly - * A copy of the child items array. This cannot be added to directly -
* components must be created using the create methods instead * components must be created using the create methods instead
@@ -70,7 +83,7 @@ declare module 'sqlops' {
* @param itemConfigs the definitions * @param itemConfigs the definitions
* @param {*} [itemLayout] Optional layout for the child items * @param {*} [itemLayout] Optional layout for the child items
*/ */
addItems(itemConfigs: Array<Component>, itemLayout ?: TItemLayout): void; addItems(itemConfigs: Array<Component>, itemLayout?: TItemLayout): void;
/** /**
* Creates a child component and adds it to this container. * Creates a child component and adds it to this container.
@@ -78,7 +91,7 @@ declare module 'sqlops' {
* @param {Component} component the component to be added * @param {Component} component the component to be added
* @param {*} [itemLayout] Optional layout for this child item * @param {*} [itemLayout] Optional layout for this child item
*/ */
addItem(component: Component, itemLayout ?: TItemLayout): void; addItem(component: Component, itemLayout?: TItemLayout): void;
/** /**
* Defines the layout for this container * Defines the layout for this container
@@ -130,9 +143,21 @@ declare module 'sqlops' {
flex?: string; flex?: string;
} }
export interface FormItemLayout {
}
export interface FormLayout {
}
export interface FlexContainer extends Container<FlexLayout, FlexItemLayout> { export interface FlexContainer extends Container<FlexLayout, FlexItemLayout> {
} }
export interface FormContainer extends Container<FormLayout, FormItemLayout> {
}
/** /**
* Describes an action to be shown in the UI, with a user-readable label * Describes an action to be shown in the UI, with a user-readable label
* and a callback to execute the action * and a callback to execute the action
@@ -153,16 +178,25 @@ declare module 'sqlops' {
* Properties representing the card component, can be used * Properties representing the card component, can be used
* when using ModelBuilder to create the component * when using ModelBuilder to create the component
*/ */
export interface CardProperties { export interface CardProperties {
label: string; label: string;
value?: string; value?: string;
actions?: ActionDescriptor[]; actions?: ActionDescriptor[];
} }
export interface InputBoxProperties { export interface InputBoxProperties {
value?: string; value?: string;
} }
export interface DropDownProperties {
value?: string;
values?: string[];
}
export interface ButtonProperties {
label?: string;
}
export interface CardComponent extends Component { export interface CardComponent extends Component {
label: string; label: string;
value: string; value: string;
@@ -174,6 +208,17 @@ declare module 'sqlops' {
onTextChanged: vscode.Event<any>; onTextChanged: vscode.Event<any>;
} }
export interface DropDownComponent extends Component {
value: string;
values: string[];
onValueChanged: vscode.Event<any>;
}
export interface ButtonComponent extends Component {
label: string;
onDidClick: vscode.Event<any>;
}
export interface WidgetComponent extends Component { export interface WidgetComponent extends Component {
widgetId: string; widgetId: string;
} }
@@ -242,6 +287,16 @@ declare module 'sqlops' {
*/ */
export function createButton(label: string): Button; export function createButton(label: string): Button;
/**
* Opens the given dialog if it is not already open
*/
export function openDialog(dialog: Dialog): void;
/**
* Closes the given dialog if it is open
*/
export function closeDialog(dialog: Dialog): void;
// Model view dialog classes // Model view dialog classes
export interface Dialog { export interface Dialog {
/** /**
@@ -252,79 +307,52 @@ declare module 'sqlops' {
/** /**
* The content of the dialog. If multiple tabs are given they will be displayed with tabs * The content of the dialog. If multiple tabs are given they will be displayed with tabs
* If a string is given, it should be the ID of the dialog's model view content * If a string is given, it should be the ID of the dialog's model view content
* TODO mairvine 4/18/18: use a model view content type
*/ */
content: string | DialogTab[], content: string | DialogTab[],
/** /**
* The caption of the OK button * The ok button
*/ */
okTitle: string; okButton: Button;
/** /**
* The caption of the Cancel button * The cancel button
*/ */
cancelTitle: string; cancelButton: Button;
/** /**
* Any additional buttons that should be displayed * Any additional buttons that should be displayed
*/ */
customButtons: Button[]; customButtons: Button[];
/**
* Opens the dialog
*/
open(): void;
/**
* Closes the dialog
*/
close(): void;
/**
* Updates the dialog on screen to reflect changes to the buttons or content
*/
updateContent(): void;
/**
* Raised when dialog's ok button is pressed
*/
readonly onOk: vscode.Event<void>;
/**
* Raised when dialog is canceled
*/
readonly onCancel: vscode.Event<void>;
} }
export interface DialogTab { export interface DialogTab {
/** /**
* The title of the tab * The title of the tab
*/ */
title: string, title: string;
/** /**
* A string giving the ID of the tab's model view content * A string giving the ID of the tab's model view content
* TODO mairvine 4/18/18: use a model view content type
*/ */
content: string; content: string;
/**
* Updates the dialog on screen to reflect changes to the content
*/
updateContent(): void;
} }
export interface Button { export interface Button {
/** /**
* The label displayed on the button * The label displayed on the button
*/ */
label: string, label: string;
/** /**
* Whether the button is enabled * Whether the button is enabled
*/ */
enabled: boolean, enabled: boolean;
/**
* Whether the button is hidden
*/
hidden: boolean;
/** /**
* Raised when the button is clicked * Raised when the button is clicked

View File

@@ -68,14 +68,17 @@ export enum ModelComponentTypes {
FlexContainer, FlexContainer,
Card, Card,
InputBox, InputBox,
DropDown,
Button,
DashboardWidget, DashboardWidget,
DashboardWebview DashboardWebview,
Form
} }
export interface IComponentShape { export interface IComponentShape {
type: ModelComponentTypes; type: ModelComponentTypes;
id: string; id: string;
properties?: { [key: string]: any }; properties?: { [key: string]: any };
layout?: any; layout?: any;
itemConfigs?: IItemConfig[]; itemConfigs?: IItemConfig[];
} }
@@ -87,10 +90,30 @@ export interface IItemConfig {
export enum ComponentEventType { export enum ComponentEventType {
PropertiesChanged, PropertiesChanged,
onDidChange onDidChange,
onDidClick
} }
export interface IComponentEventArgs { export interface IComponentEventArgs {
eventType: ComponentEventType; eventType: ComponentEventType;
args: any; args: any;
} }
export interface IModelViewDialogDetails {
title: string;
content: string | number[];
okButton: number;
cancelButton: number;
customButtons: number[];
}
export interface IModelViewTabDetails {
title: string;
content: string;
}
export interface IModelViewButtonDetails {
label: string;
enabled: boolean;
hidden: boolean;
}

View File

@@ -33,6 +33,11 @@ class ModelBuilderImpl implements sqlops.ModelBuilder {
return new ContainerBuilderImpl<sqlops.FlexContainer, sqlops.FlexLayout, sqlops.FlexItemLayout>(this._proxy, this._handle, ModelComponentTypes.FlexContainer, id); return new ContainerBuilderImpl<sqlops.FlexContainer, sqlops.FlexLayout, sqlops.FlexItemLayout>(this._proxy, this._handle, ModelComponentTypes.FlexContainer, id);
} }
formContainer(): sqlops.FormBuilder {
let id = this.getNextComponentId();
return new FormContainerBuilder(this._proxy, this._handle, ModelComponentTypes.Form, id);
}
card(): sqlops.ComponentBuilder<sqlops.CardComponent> { card(): sqlops.ComponentBuilder<sqlops.CardComponent> {
let id = this.getNextComponentId(); let id = this.getNextComponentId();
return this.withEventHandler(new CardWrapper(this._proxy, this._handle, id), id); return this.withEventHandler(new CardWrapper(this._proxy, this._handle, id), id);
@@ -43,6 +48,16 @@ class ModelBuilderImpl implements sqlops.ModelBuilder {
return this.withEventHandler(new InputBoxWrapper(this._proxy, this._handle, id), id); return this.withEventHandler(new InputBoxWrapper(this._proxy, this._handle, id), id);
} }
button(): sqlops.ComponentBuilder<sqlops.ButtonComponent> {
let id = this.getNextComponentId();
return this.withEventHandler(new ButtonWrapper(this._proxy, this._handle, id), id);
}
dropDown(): sqlops.ComponentBuilder<sqlops.DropDownComponent> {
let id = this.getNextComponentId();
return this.withEventHandler(new DropDownWrapper(this._proxy, this._handle, id), id);
}
dashboardWidget(widgetId: string): sqlops.ComponentBuilder<sqlops.WidgetComponent> { dashboardWidget(widgetId: string): sqlops.ComponentBuilder<sqlops.WidgetComponent> {
let id = this.getNextComponentId(); let id = this.getNextComponentId();
return this.withEventHandler<sqlops.WidgetComponent>(new ComponentWrapper(this._proxy, this._handle, ModelComponentTypes.DashboardWidget, id), id); return this.withEventHandler<sqlops.WidgetComponent>(new ComponentWrapper(this._proxy, this._handle, ModelComponentTypes.DashboardWidget, id), id);
@@ -122,9 +137,40 @@ class ContainerBuilderImpl<T extends sqlops.Component, TLayout, TItemLayout> ext
} }
} }
class FormContainerBuilder extends ContainerBuilderImpl<sqlops.FormContainer, sqlops.FormLayout, sqlops.FormItemLayout> {
withFormItems(components: sqlops.FormComponent[], itemLayout?: sqlops.FormItemLayout): sqlops.ContainerBuilder<sqlops.FormContainer, sqlops.FormLayout, sqlops.FormItemLayout> {
this._component.itemConfigs = components.map(item => {
let componentWrapper = item.component as ComponentWrapper;
let actions: string[] = undefined;
if (item.actions) {
actions = item.actions.map(action => {
let actionComponentWrapper = action as ComponentWrapper;
return actionComponentWrapper.id;
});
}
return new InternalItemConfig(componentWrapper, Object.assign({}, itemLayout, {
title: item.title,
actions: actions,
isFormComponent: true
}));
});
components.forEach(formItem => {
if (formItem.actions) {
formItem.actions.forEach(component => {
let componentWrapper = component as ComponentWrapper;
this._component.itemConfigs.push(new InternalItemConfig(componentWrapper, itemLayout));
});
}
});
return this;
}
}
class InternalItemConfig { class InternalItemConfig {
constructor(private _component: ComponentWrapper, public config: any) {} constructor(private _component: ComponentWrapper, public config: any) { }
public toIItemConfig(): IItemConfig { public toIItemConfig(): IItemConfig {
return { return {
@@ -146,6 +192,7 @@ class ComponentWrapper implements sqlops.Component {
private _onErrorEmitter = new Emitter<Error>(); private _onErrorEmitter = new Emitter<Error>();
public readonly onError: vscode.Event<Error> = this._onErrorEmitter.event; public readonly onError: vscode.Event<Error> = this._onErrorEmitter.event;
protected _emitterMap = new Map<ComponentEventType, Emitter<any>>();
constructor(protected readonly _proxy: MainThreadModelViewShape, constructor(protected readonly _proxy: MainThreadModelViewShape,
protected readonly _handle: number, protected readonly _handle: number,
@@ -169,7 +216,7 @@ class ComponentWrapper implements sqlops.Component {
} }
public toComponentShape(): IComponentShape { public toComponentShape(): IComponentShape {
return <IComponentShape> { return <IComponentShape>{
id: this.id, id: this.id,
type: this.type, type: this.type,
layout: this.layout, layout: this.layout,
@@ -183,13 +230,13 @@ class ComponentWrapper implements sqlops.Component {
return this._proxy.$clearContainer(this._handle, this.id); return this._proxy.$clearContainer(this._handle, this.id);
} }
public addItems(items: Array<sqlops.Component>, itemLayout ?: any): void { public addItems(items: Array<sqlops.Component>, itemLayout?: any): void {
for(let item of items) { for (let item of items) {
this.addItem(item, itemLayout); this.addItem(item, itemLayout);
} }
} }
public addItem(item: sqlops.Component, itemLayout ?: any): void { public addItem(item: sqlops.Component, itemLayout?: any): void {
let itemImpl = item as ComponentWrapper; let itemImpl = item as ComponentWrapper;
if (!itemImpl) { if (!itemImpl) {
throw new Error(nls.localize('unknownComponentType', 'Unkown component type. Must use ModelBuilder to create objects')); throw new Error(nls.localize('unknownComponentType', 'Unkown component type. Must use ModelBuilder to create objects'));
@@ -218,7 +265,12 @@ class ComponentWrapper implements sqlops.Component {
public onEvent(eventArgs: IComponentEventArgs) { public onEvent(eventArgs: IComponentEventArgs) {
if (eventArgs && eventArgs.eventType === ComponentEventType.PropertiesChanged) { if (eventArgs && eventArgs.eventType === ComponentEventType.PropertiesChanged) {
this.properties = eventArgs.args; this.properties = eventArgs.args;
} } else if (eventArgs) {
let emitter = this._emitterMap.get(eventArgs.eventType);
if (emitter) {
emitter.fire();
}
}
} }
protected setProperty(key: string, value: any): Thenable<boolean> { protected setProperty(key: string, value: any): Thenable<boolean> {
@@ -278,9 +330,6 @@ class InputBoxWrapper extends ComponentWrapper implements sqlops.InputBoxCompone
this._emitterMap.set(ComponentEventType.onDidChange, new Emitter<any>()); this._emitterMap.set(ComponentEventType.onDidChange, new Emitter<any>());
} }
private _onTextChangedEmitter = new Emitter<any>();
private _emitterMap = new Map<ComponentEventType, Emitter<any>>();
public get value(): string { public get value(): string {
return this.properties['value']; return this.properties['value'];
} }
@@ -292,15 +341,54 @@ class InputBoxWrapper extends ComponentWrapper implements sqlops.InputBoxCompone
let emitter = this._emitterMap.get(ComponentEventType.onDidChange); let emitter = this._emitterMap.get(ComponentEventType.onDidChange);
return emitter && emitter.event; return emitter && emitter.event;
} }
}
public onEvent(eventArgs: IComponentEventArgs) { class DropDownWrapper extends ComponentWrapper implements sqlops.DropDownComponent {
super.onEvent(eventArgs);
if (eventArgs) { constructor(proxy: MainThreadModelViewShape, handle: number, id: string) {
let emitter = this._emitterMap.get(eventArgs.eventType); super(proxy, handle, ModelComponentTypes.DropDown, id);
if (emitter) { this.properties = {};
emitter.fire(); this._emitterMap.set(ComponentEventType.onDidChange, new Emitter<any>());
} }
}
public get value(): string {
return this.properties['value'];
}
public set value(v: string) {
this.setProperty('value', v);
}
public get values(): string[] {
return this.properties['values'];
}
public set values(v: string[]) {
this.setProperty('values', v);
}
public get onValueChanged(): vscode.Event<any> {
let emitter = this._emitterMap.get(ComponentEventType.onDidChange);
return emitter && emitter.event;
}
}
class ButtonWrapper extends ComponentWrapper implements sqlops.ButtonComponent {
constructor(proxy: MainThreadModelViewShape, handle: number, id: string) {
super(proxy, handle, ModelComponentTypes.Button, id);
this.properties = {};
this._emitterMap.set(ComponentEventType.onDidClick, new Emitter<any>());
}
public get label(): string {
return this.properties['label'];
}
public set label(v: string) {
this.setProperty('label', v);
}
public get onDidClick(): vscode.Event<any> {
let emitter = this._emitterMap.get(ComponentEventType.onDidClick);
return emitter && emitter.event;
} }
} }

View File

@@ -0,0 +1,211 @@
/*---------------------------------------------------------------------------------------------
* 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 { IMainContext } from 'vs/workbench/api/node/extHost.protocol';
import Event, { Emitter } from 'vs/base/common/event';
import { deepClone } from 'vs/base/common/objects';
import * as nls from 'vs/nls';
import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
import { SqlMainContext, ExtHostModelViewDialogShape, MainThreadModelViewDialogShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { IItemConfig, ModelComponentTypes, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes';
class DialogImpl implements sqlops.window.modelviewdialog.Dialog {
public title: string;
public content: string | sqlops.window.modelviewdialog.DialogTab[];
public okButton: sqlops.window.modelviewdialog.Button;
public cancelButton: sqlops.window.modelviewdialog.Button;
public customButtons: sqlops.window.modelviewdialog.Button[];
constructor(private _extHostModelViewDialog: ExtHostModelViewDialog) {
this.okButton = this._extHostModelViewDialog.createButton(nls.localize('dialogOkLabel', 'Done'));
this.cancelButton = this._extHostModelViewDialog.createButton(nls.localize('dialogCancelLabel', 'Cancel'));
}
}
class TabImpl implements sqlops.window.modelviewdialog.DialogTab {
public title: string;
public content: string;
constructor(private _extHostModelViewDialog: ExtHostModelViewDialog) { }
}
class ButtonImpl implements sqlops.window.modelviewdialog.Button {
private _label: string;
private _enabled: boolean;
private _hidden: boolean;
private _onClick = new Emitter<void>();
public onClick = this._onClick.event;
constructor(private _extHostModelViewDialog: ExtHostModelViewDialog) {
this._enabled = true;
this._hidden = false;
}
public get label(): string {
return this._label;
}
public set label(label: string) {
this._label = label;
this._extHostModelViewDialog.updateButton(this);
}
public get enabled(): boolean {
return this._enabled;
}
public set enabled(enabled: boolean) {
this._enabled = enabled;
this._extHostModelViewDialog.updateButton(this);
}
public get hidden(): boolean {
return this._hidden;
}
public set hidden(hidden: boolean) {
this._hidden = hidden;
this._extHostModelViewDialog.updateButton(this);
}
public getOnClickCallback(): () => void {
return () => this._onClick.fire();
}
}
export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape {
private static _currentHandle = 0;
private readonly _proxy: MainThreadModelViewDialogShape;
private readonly _dialogHandles = new Map<sqlops.window.modelviewdialog.Dialog, number>();
private readonly _tabHandles = new Map<sqlops.window.modelviewdialog.DialogTab, number>();
private readonly _buttonHandles = new Map<sqlops.window.modelviewdialog.Button, number>();
private readonly _onClickCallbacks = new Map<number, () => void>();
constructor(
mainContext: IMainContext
) {
this._proxy = mainContext.getProxy(SqlMainContext.MainThreadModelViewDialog);
}
private static getNewHandle() {
let handle = ExtHostModelViewDialog._currentHandle;
ExtHostModelViewDialog._currentHandle += 1;
return handle;
}
private getDialogHandle(dialog: sqlops.window.modelviewdialog.Dialog) {
let handle = this._dialogHandles.get(dialog);
if (handle === undefined) {
handle = ExtHostModelViewDialog.getNewHandle();
this._dialogHandles.set(dialog, handle);
}
return handle;
}
private getTabHandle(tab: sqlops.window.modelviewdialog.DialogTab) {
let handle = this._tabHandles.get(tab);
if (handle === undefined) {
handle = ExtHostModelViewDialog.getNewHandle();
this._tabHandles.set(tab, handle);
}
return handle;
}
private getButtonHandle(button: sqlops.window.modelviewdialog.Button) {
let handle = this._buttonHandles.get(button);
if (handle === undefined) {
handle = ExtHostModelViewDialog.getNewHandle();
this._buttonHandles.set(button, handle);
}
return handle;
}
public $onButtonClick(handle: number): void {
this._onClickCallbacks.get(handle)();
}
public open(dialog: sqlops.window.modelviewdialog.Dialog): void {
let handle = this.getDialogHandle(dialog);
this.updateDialogContent(dialog);
this._proxy.$open(handle);
}
public close(dialog: sqlops.window.modelviewdialog.Dialog): void {
let handle = this.getDialogHandle(dialog);
this._proxy.$close(handle);
}
public updateDialogContent(dialog: sqlops.window.modelviewdialog.Dialog): void {
let handle = this.getDialogHandle(dialog);
let tabs = dialog.content;
if (tabs && typeof tabs !== 'string') {
tabs.forEach(tab => this.updateTabContent(tab));
}
if (dialog.customButtons) {
dialog.customButtons.forEach(button => this.updateButton(button));
}
this.updateButton(dialog.okButton);
this.updateButton(dialog.cancelButton);
this._proxy.$setDialogDetails(handle, {
title: dialog.title,
okButton: this.getButtonHandle(dialog.okButton),
cancelButton: this.getButtonHandle(dialog.cancelButton),
content: dialog.content && typeof dialog.content !== 'string' ? dialog.content.map(tab => this.getTabHandle(tab)) : dialog.content as string,
customButtons: dialog.customButtons ? dialog.customButtons.map(button => this.getButtonHandle(button)) : undefined
});
}
public updateTabContent(tab: sqlops.window.modelviewdialog.DialogTab): void {
let handle = this.getTabHandle(tab);
this._proxy.$setTabDetails(handle, {
title: tab.title,
content: tab.content
});
}
public updateButton(button: sqlops.window.modelviewdialog.Button): void {
let handle = this.getButtonHandle(button);
this._proxy.$setButtonDetails(handle, {
label: button.label,
enabled: button.enabled,
hidden: button.hidden
});
}
public registerOnClickCallback(button: sqlops.window.modelviewdialog.Button, callback: () => void) {
let handle = this.getButtonHandle(button);
this._onClickCallbacks.set(handle, callback);
}
public createDialog(title: string): sqlops.window.modelviewdialog.Dialog {
let dialog = new DialogImpl(this);
dialog.title = title;
this.getDialogHandle(dialog);
return dialog;
}
public createTab(title: string): sqlops.window.modelviewdialog.DialogTab {
let tab = new TabImpl(this);
tab.title = title;
this.getTabHandle(tab);
return tab;
}
public createButton(label: string): sqlops.window.modelviewdialog.Button {
let button = new ButtonImpl(this);
this.getButtonHandle(button);
this.registerOnClickCallback(button, button.getOnClickCallback());
button.label = label;
return button;
}
}

View File

@@ -45,7 +45,7 @@ export class MainThreadModelView extends Disposable implements MainThreadModelVi
} }
$initializeModel(handle: number, rootComponent: IComponentShape): Thenable<void> { $initializeModel(handle: number, rootComponent: IComponentShape): Thenable<void> {
return this.execModelViewAction(handle, (modelView) => { return this.execModelViewAction(handle, (modelView) => {
modelView.initializeModel(rootComponent); modelView.initializeModel(rootComponent);
}); });
} }
@@ -67,11 +67,13 @@ export class MainThreadModelView extends Disposable implements MainThreadModelVi
this._proxy.$handleEvent(handle, componentId, eventArgs); this._proxy.$handleEvent(handle, componentId, eventArgs);
} }
$registerEvent(handle: number, componentId: string): Thenable<void> { $registerEvent(handle: number, componentId: string): Thenable<void> {
let properties: { [key: string]: any; } = { eventName: this.onEvent }; let properties: { [key: string]: any; } = { eventName: this.onEvent };
return this.execModelViewAction(handle, (modelView) => { return this.execModelViewAction(handle, (modelView) => {
this._register(modelView.onEvent (e => { this._register(modelView.onEvent(e => {
this.onEvent(handle, componentId, e); if (e.componentId && e.componentId === componentId) {
this.onEvent(handle, componentId, e);
}
})); }));
}); });
} }

View File

@@ -0,0 +1,130 @@
/*---------------------------------------------------------------------------------------------
* 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 { MainThreadModelViewDialogShape, SqlMainContext, ExtHostModelViewDialogShape, SqlExtHostContext } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
import { Dialog, DialogTab, DialogButton } from 'sql/platform/dialog/dialogTypes';
import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol';
import { CustomDialogService } from 'sql/platform/dialog/customDialogService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IModelViewDialogDetails, IModelViewTabDetails, IModelViewButtonDetails } from 'sql/workbench/api/common/sqlExtHostTypes';
@extHostNamedCustomer(SqlMainContext.MainThreadModelViewDialog)
export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape {
private readonly _proxy: ExtHostModelViewDialogShape;
private readonly _dialogs = new Map<number, Dialog>();
private readonly _tabs = new Map<number, DialogTab>();
private readonly _buttons = new Map<number, DialogButton>();
private _dialogService: CustomDialogService;
constructor(
context: IExtHostContext,
@IInstantiationService instatiationService: IInstantiationService,
) {
this._proxy = context.getProxy(SqlExtHostContext.ExtHostModelViewDialog);
this._dialogService = new CustomDialogService(instatiationService);
}
public dispose(): void {
throw new Error('Method not implemented.');
}
public $open(handle: number): Thenable<void> {
let dialog = this.getDialog(handle);
this._dialogService.showDialog(dialog);
return Promise.resolve();
}
public $close(handle: number): Thenable<void> {
let dialog = this.getDialog(handle);
this._dialogService.closeDialog(dialog);
return Promise.resolve();
}
public $setDialogDetails(handle: number, details: IModelViewDialogDetails): Thenable<void> {
let dialog = this._dialogs.get(handle);
if (!dialog) {
dialog = new Dialog(details.title);
let okButton = this.getButton(details.okButton);
let cancelButton = this.getButton(details.cancelButton);
dialog.okButton = okButton;
dialog.cancelButton = cancelButton;
this._dialogs.set(handle, dialog);
}
dialog.title = details.title;
if (details.content && typeof details.content !== 'string') {
dialog.content = details.content.map(tabHandle => this.getTab(tabHandle));
} else {
dialog.content = details.content as string;
}
if (details.customButtons) {
dialog.customButtons = details.customButtons.map(buttonHandle => this.getButton(buttonHandle));
}
return Promise.resolve();
}
public $setTabDetails(handle: number, details: IModelViewTabDetails): Thenable<void> {
let tab = this._tabs.get(handle);
if (!tab) {
tab = new DialogTab(details.title);
this._tabs.set(handle, tab);
}
tab.title = details.title;
tab.content = details.content;
return Promise.resolve();
}
public $setButtonDetails(handle: number, details: IModelViewButtonDetails): Thenable<void> {
let button = this._buttons.get(handle);
if (!button) {
button = new DialogButton(details.label, details.enabled);
button.hidden = details.hidden;
button.onClick(() => this.onButtonClick(handle));
this._buttons.set(handle, button);
} else {
button.label = details.label;
button.enabled = details.enabled;
button.hidden = details.hidden;
}
return Promise.resolve();
}
private getDialog(handle: number): Dialog {
let dialog = this._dialogs.get(handle);
if (!dialog) {
throw new Error('No dialog matching the given handle');
}
return dialog;
}
private getTab(handle: number): DialogTab {
let tab = this._tabs.get(handle);
if (!tab) {
throw new Error('No tab matching the given handle');
}
return tab;
}
private getButton(handle: number): DialogButton {
let button = this._buttons.get(handle);
if (!button) {
throw new Error('No button matching the given handle');
}
return button;
}
private onButtonClick(handle: number): void {
this._proxy.$onButtonClick(handle);
}
}

View File

@@ -32,6 +32,7 @@ import { ExtHostConnectionManagement } from 'sql/workbench/api/node/extHostConne
import { ExtHostDashboard } from 'sql/workbench/api/node/extHostDashboard'; import { ExtHostDashboard } from 'sql/workbench/api/node/extHostDashboard';
import { ExtHostObjectExplorer } from 'sql/workbench/api/node/extHostObjectExplorer'; import { ExtHostObjectExplorer } from 'sql/workbench/api/node/extHostObjectExplorer';
import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService';
import { ExtHostModelViewDialog } from 'sql/workbench/api/node/extHostModelViewDialog';
import { ExtHostQueryEditor } from 'sql/workbench/api/node/extHostQueryEditor'; import { ExtHostQueryEditor } from 'sql/workbench/api/node/extHostQueryEditor';
export interface ISqlExtensionApiFactory { export interface ISqlExtensionApiFactory {
@@ -65,6 +66,7 @@ export function createApiFactory(
const extHostWebviewWidgets = rpcProtocol.set(SqlExtHostContext.ExtHostDashboardWebviews, new ExtHostDashboardWebviews(rpcProtocol)); const extHostWebviewWidgets = rpcProtocol.set(SqlExtHostContext.ExtHostDashboardWebviews, new ExtHostDashboardWebviews(rpcProtocol));
const extHostModelView = rpcProtocol.set(SqlExtHostContext.ExtHostModelView, new ExtHostModelView(rpcProtocol)); const extHostModelView = rpcProtocol.set(SqlExtHostContext.ExtHostModelView, new ExtHostModelView(rpcProtocol));
const extHostDashboard = rpcProtocol.set(SqlExtHostContext.ExtHostDashboard, new ExtHostDashboard(rpcProtocol)); const extHostDashboard = rpcProtocol.set(SqlExtHostContext.ExtHostDashboard, new ExtHostDashboard(rpcProtocol));
const extHostModelViewDialog = rpcProtocol.set(SqlExtHostContext.ExtHostModelViewDialog, new ExtHostModelViewDialog(rpcProtocol));
const extHostQueryEditor = rpcProtocol.set(SqlExtHostContext.ExtHostQueryEditor, new ExtHostQueryEditor(rpcProtocol)); const extHostQueryEditor = rpcProtocol.set(SqlExtHostContext.ExtHostQueryEditor, new ExtHostQueryEditor(rpcProtocol));
@@ -283,10 +285,21 @@ export function createApiFactory(
}; };
const modelViewDialog: typeof sqlops.window.modelviewdialog = { const modelViewDialog: typeof sqlops.window.modelviewdialog = {
// TODO mairvine 4/18/18: Implement the extension layer for custom dialogs createDialog(title: string): sqlops.window.modelviewdialog.Dialog {
createDialog(title: string): sqlops.window.modelviewdialog.Dialog { return undefined; }, return extHostModelViewDialog.createDialog(title);
createTab(title: string): sqlops.window.modelviewdialog.DialogTab { return undefined; }, },
createButton(label: string): sqlops.window.modelviewdialog.Button { return undefined; } createTab(title: string): sqlops.window.modelviewdialog.DialogTab {
return extHostModelViewDialog.createTab(title);
},
createButton(label: string): sqlops.window.modelviewdialog.Button {
return extHostModelViewDialog.createButton(label);
},
openDialog(dialog: sqlops.window.modelviewdialog.Dialog) {
return extHostModelViewDialog.open(dialog);
},
closeDialog(dialog: sqlops.window.modelviewdialog.Dialog) {
return extHostModelViewDialog.close(dialog);
}
}; };
const window: typeof sqlops.window = { const window: typeof sqlops.window = {

View File

@@ -21,6 +21,7 @@ import 'sql/workbench/api/electron-browser/mainThreadDashboard';
import 'sql/workbench/api/node/mainThreadDashboardWebview'; import 'sql/workbench/api/node/mainThreadDashboardWebview';
import 'sql/workbench/api/node/mainThreadQueryEditor'; import 'sql/workbench/api/node/mainThreadQueryEditor';
import 'sql/workbench/api/node/mainThreadModelView'; import 'sql/workbench/api/node/mainThreadModelView';
import 'sql/workbench/api/node/mainThreadModelViewDialog';
import './mainThreadAccountManagement'; import './mainThreadAccountManagement';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';

View File

@@ -16,7 +16,7 @@ import * as sqlops from 'sqlops';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { ITaskHandlerDescription } from 'sql/platform/tasks/common/tasks'; import { ITaskHandlerDescription } from 'sql/platform/tasks/common/tasks';
import { IItemConfig, ModelComponentTypes, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes'; import { IItemConfig, ModelComponentTypes, IComponentShape, IModelViewDialogDetails, IModelViewTabDetails, IModelViewButtonDetails } from 'sql/workbench/api/common/sqlExtHostTypes';
import Event, { Emitter } from 'vs/base/common/event'; import Event, { Emitter } from 'vs/base/common/event';
export abstract class ExtHostAccountManagementShape { export abstract class ExtHostAccountManagementShape {
@@ -448,6 +448,7 @@ export const SqlMainContext = {
MainThreadDashboardWebview: createMainId<MainThreadDashboardWebviewShape>('MainThreadDashboardWebview'), MainThreadDashboardWebview: createMainId<MainThreadDashboardWebviewShape>('MainThreadDashboardWebview'),
MainThreadModelView: createMainId<MainThreadModelViewShape>('MainThreadModelView'), MainThreadModelView: createMainId<MainThreadModelViewShape>('MainThreadModelView'),
MainThreadDashboard: createMainId<MainThreadDashboardShape>('MainThreadDashboard'), MainThreadDashboard: createMainId<MainThreadDashboardShape>('MainThreadDashboard'),
MainThreadModelViewDialog: createMainId<MainThreadModelViewDialogShape>('MainThreadModelViewDialog'),
MainThreadQueryEditor: createMainId<MainThreadQueryEditorShape>('MainThreadQueryEditor'), MainThreadQueryEditor: createMainId<MainThreadQueryEditorShape>('MainThreadQueryEditor'),
}; };
@@ -464,6 +465,7 @@ export const SqlExtHostContext = {
ExtHostDashboardWebviews: createExtId<ExtHostDashboardWebviewsShape>('ExtHostDashboardWebviews'), ExtHostDashboardWebviews: createExtId<ExtHostDashboardWebviewsShape>('ExtHostDashboardWebviews'),
ExtHostModelView: createExtId<ExtHostModelViewShape>('ExtHostModelView'), ExtHostModelView: createExtId<ExtHostModelViewShape>('ExtHostModelView'),
ExtHostDashboard: createExtId<ExtHostDashboardShape>('ExtHostDashboard'), ExtHostDashboard: createExtId<ExtHostDashboardShape>('ExtHostDashboard'),
ExtHostModelViewDialog: createExtId<ExtHostModelViewDialogShape>('ExtHostModelViewDialog'),
ExtHostQueryEditor: createExtId<ExtHostQueryEditorShape>('ExtHostQueryEditor') ExtHostQueryEditor: createExtId<ExtHostQueryEditorShape>('ExtHostQueryEditor')
}; };
@@ -543,10 +545,21 @@ export interface MainThreadObjectExplorerShape extends IDisposable {
$findNodes(connectionId: string, type: string, schema: string, name: string, database: string, parentObjectNames: string[]): Thenable<sqlops.NodeInfo[]>; $findNodes(connectionId: string, type: string, schema: string, name: string, database: string, parentObjectNames: string[]): Thenable<sqlops.NodeInfo[]>;
} }
export interface ExtHostModelViewDialogShape {
$onButtonClick(handle: number): void;
}
export interface MainThreadModelViewDialogShape extends IDisposable {
$open(handle: number): Thenable<void>;
$close(handle: number): Thenable<void>;
$setDialogDetails(handle: number, details: IModelViewDialogDetails): Thenable<void>;
$setTabDetails(handle: number, details: IModelViewTabDetails): Thenable<void>;
$setButtonDetails(handle: number, details: IModelViewButtonDetails): Thenable<void>;
}
export interface ExtHostQueryEditorShape { export interface ExtHostQueryEditorShape {
} }
export interface MainThreadQueryEditorShape extends IDisposable { export interface MainThreadQueryEditorShape extends IDisposable {
$connect(fileUri: string, connectionId: string): Thenable<void>; $connect(fileUri: string, connectionId: string): Thenable<void>;
$runQuery(fileUri: string): void; $runQuery(fileUri: string): void;
} }

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* 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 assert from 'assert';
import * as SharedServices from 'sql/parts/grid/services/sharedServices';
const testText = '<div>test text</div>';
suite('Grid shared services tests', () => {
test('textFormatter should encode HTML when formatting a DBCellValue object', () => {
// If I format a DBCellValue object that contains HTML
let cellValue = new SharedServices.DBCellValue();
cellValue.displayValue = testText;
cellValue.isNull = false;
let formattedHtml = SharedServices.textFormatter(undefined, undefined, cellValue, undefined, undefined);
// Then the result is HTML for a span element containing the cell value's display value as plain text
verifyFormattedHtml(formattedHtml, testText);
});
test('textFormatter should encode HTML when formatting a string', () => {
// If I format a string that contains HTML
let formattedHtml = SharedServices.textFormatter(undefined, undefined, testText, undefined, undefined);
// Then the result is HTML for a span element containing the given text as plain text
verifyFormattedHtml(formattedHtml, testText);
});
});
function verifyFormattedHtml(formattedHtml: string, expectedText: string): void {
// Create an element containing the span returned by the format call
let element = document.createElement('div');
element.innerHTML = formattedHtml;
let spanElement = element.children[0];
// Verify that the span element's text, not its innerHTML, matches the expected text
assert.equal(spanElement.textContent, testText);
assert.notEqual(spanElement.innerHTML, testText);
}

View File

@@ -3,7 +3,7 @@
* 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 { Dialog, DialogTab } from 'sql/platform/dialog/dialogTypes'; import { Dialog, DialogTab } from 'sql/platform/dialog/dialogTypes';
import { Mock, It, Times } from 'typemoq'; import { Mock, It, Times } from 'typemoq';
import { IBootstrapService } from 'sql/services/bootstrap/bootstrapService'; import { IBootstrapService } from 'sql/services/bootstrap/bootstrapService';
import { DialogPane } from 'sql/platform/dialog/dialogPane'; import { DialogPane } from 'sql/platform/dialog/dialogPane';

View File

@@ -0,0 +1,121 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Mock, It, Times } from 'typemoq';
import { ExtHostModelViewDialog } from 'sql/workbench/api/node/extHostModelViewDialog';
import { MainThreadModelViewDialogShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { IMainContext } from 'vs/workbench/api/node/extHost.protocol';
'use strict';
suite('ExtHostModelViewDialog Tests', () => {
let extHostModelViewDialog: ExtHostModelViewDialog;
let mockProxy: Mock<MainThreadModelViewDialogShape>;
setup(() => {
mockProxy = Mock.ofInstance(<MainThreadModelViewDialogShape>{
$open: handle => undefined,
$close: handle => undefined,
$setDialogDetails: (handle, details) => undefined,
$setTabDetails: (handle, details) => undefined,
$setButtonDetails: (handle, details) => undefined
});
let mainContext = <IMainContext>{
getProxy: proxyType => mockProxy.object
};
extHostModelViewDialog = new ExtHostModelViewDialog(mainContext);
});
test('Creating a dialog returns a dialog with initialized ok and cancel buttons and the given title', () => {
let title = 'dialog_title';
let dialog = extHostModelViewDialog.createDialog(title);
assert.equal(dialog.title, title);
assert.equal(dialog.okButton.enabled, true);
assert.equal(dialog.cancelButton.enabled, true);
});
test('Creating a tab returns a tab with the given title', () => {
let title = 'tab_title';
let tab = extHostModelViewDialog.createTab(title);
assert.equal(tab.title, title);
});
test('Creating a button returns an enabled button with the given label', () => {
let label = 'button_label';
let button = extHostModelViewDialog.createButton(label);
assert.equal(button.label, label);
assert.equal(button.enabled, true);
});
test('Opening a dialog updates its tabs and buttons on the main thread', () => {
mockProxy.setup(x => x.$open(It.isAny()));
mockProxy.setup(x => x.$setDialogDetails(It.isAny(), It.isAny()));
mockProxy.setup(x => x.$setTabDetails(It.isAny(), It.isAny()));
mockProxy.setup(x => x.$setButtonDetails(It.isAny(), It.isAny()));
// Create a dialog with 2 tabs and 2 custom buttons
let dialogTitle = 'dialog_title';
let dialog = extHostModelViewDialog.createDialog(dialogTitle);
let tab1Title = 'tab_1';
let tab1 = extHostModelViewDialog.createTab(tab1Title);
let tab2Title = 'tab_2';
let tab2 = extHostModelViewDialog.createTab(tab2Title);
dialog.content = [tab1, tab2];
let button1Label = 'button_1';
let button1 = extHostModelViewDialog.createButton(button1Label);
button1.enabled = false;
let button2Label = 'button_2';
let button2 = extHostModelViewDialog.createButton(button2Label);
// Open the dialog and verify that the correct main thread methods were called
extHostModelViewDialog.open(dialog);
mockProxy.verify(x => x.$setButtonDetails(It.isAny(), It.is(details => {
return details.enabled === false && details.label === button1Label;
})), Times.once());
mockProxy.verify(x => x.$setButtonDetails(It.isAny(), It.is(details => {
return details.enabled === true && details.label === button2Label;
})), Times.once());
mockProxy.verify(x => x.$setTabDetails(It.isAny(), It.is(details => {
return details.title === tab1Title;
})), Times.once());
mockProxy.verify(x => x.$setTabDetails(It.isAny(), It.is(details => {
return details.title === tab2Title;
})), Times.once());
mockProxy.verify(x => x.$setDialogDetails(It.isAny(), It.is(details => {
return details.title === dialogTitle;
})), Times.once());
mockProxy.verify(x => x.$open(It.isAny()), Times.once());
});
test('Button clicks are forwarded to the correct button', () => {
// Set up the proxy to record button handles
let handles = [];
mockProxy.setup(x => x.$setButtonDetails(It.isAny(), It.isAny())).callback((handle, details) => handles.push(handle));
// Set up the buttons to record click events
let label1 = 'button_1';
let label2 = 'button_2';
let button1 = extHostModelViewDialog.createButton(label1);
let button2 = extHostModelViewDialog.createButton(label2);
let clickEvents = [];
button1.onClick(() => clickEvents.push(1));
button2.onClick(() => clickEvents.push(2));
extHostModelViewDialog.updateButton(button1);
extHostModelViewDialog.updateButton(button2);
// If the main thread sends some notifications that the buttons have been clicked
extHostModelViewDialog.$onButtonClick(handles[0]);
extHostModelViewDialog.$onButtonClick(handles[1]);
extHostModelViewDialog.$onButtonClick(handles[1]);
extHostModelViewDialog.$onButtonClick(handles[0]);
// Then the clicks should have been handled by the expected handlers
assert.deepEqual(clickEvents, [1, 2, 2, 1]);
});
});

View File

@@ -0,0 +1,157 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Mock, It, Times } from 'typemoq';
import { MainThreadModelViewDialog } from 'sql/workbench/api/node/mainThreadModelViewDialog';
import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol';
import { IModelViewButtonDetails, IModelViewTabDetails, IModelViewDialogDetails } from 'sql/workbench/api/common/sqlExtHostTypes';
import { CustomDialogService } from 'sql/platform/dialog/customDialogService';
import { Dialog, DialogTab } from 'sql/platform/dialog/dialogTypes';
import { ExtHostModelViewDialogShape } from 'sql/workbench/api/node/sqlExtHost.protocol';
import { Emitter } from 'vs/base/common/event';
'use strict';
suite('MainThreadModelViewDialog Tests', () => {
let mainThreadModelViewDialog: MainThreadModelViewDialog;
let mockExtHostModelViewDialog: Mock<ExtHostModelViewDialogShape>;
let mockDialogService: Mock<CustomDialogService>;
let openedDialog: Dialog;
// Dialog details
let button1Details: IModelViewButtonDetails;
let button2Details: IModelViewButtonDetails;
let okButtonDetails: IModelViewButtonDetails;
let cancelButtonDetails: IModelViewButtonDetails;
let tab1Details: IModelViewTabDetails;
let tab2Details: IModelViewTabDetails;
let dialogDetails: IModelViewDialogDetails;
let button1Handle = 1;
let button2Handle = 2;
let okButtonHandle = 3;
let cancelButtonHandle = 4;
let tab1Handle = 5;
let tab2Handle = 6;
let dialogHandle = 7;
setup(() => {
mockExtHostModelViewDialog = Mock.ofInstance(<ExtHostModelViewDialogShape>{
$onButtonClick: handle => undefined
});
let extHostContext = <IExtHostContext>{
getProxy: proxyType => mockExtHostModelViewDialog.object
};
mainThreadModelViewDialog = new MainThreadModelViewDialog(extHostContext, undefined);
// Set up the mock dialog service
mockDialogService = Mock.ofType(CustomDialogService, undefined, undefined);
openedDialog = undefined;
mockDialogService.setup(x => x.showDialog(It.isAny())).callback(dialog => openedDialog = dialog);
(mainThreadModelViewDialog as any)._dialogService = mockDialogService.object;
// Set up the dialog details
button1Details = {
label: 'button1',
enabled: false,
hidden: false
};
button2Details = {
label: 'button2',
enabled: true,
hidden: false
};
okButtonDetails = {
label: 'ok_label',
enabled: true,
hidden: false
};
cancelButtonDetails = {
label: 'cancel_label',
enabled: true,
hidden: false
};
tab1Details = {
title: 'tab1',
content: 'content1'
};
tab2Details = {
title: 'tab2',
content: 'content2'
};
dialogDetails = {
title: 'dialog1',
content: [tab1Handle, tab2Handle],
okButton: okButtonHandle,
cancelButton: cancelButtonHandle,
customButtons: [button1Handle, button2Handle]
};
// Register the buttons, tabs, and dialog
mainThreadModelViewDialog.$setButtonDetails(button1Handle, button1Details);
mainThreadModelViewDialog.$setButtonDetails(button2Handle, button2Details);
mainThreadModelViewDialog.$setButtonDetails(okButtonHandle, okButtonDetails);
mainThreadModelViewDialog.$setButtonDetails(cancelButtonHandle, cancelButtonDetails);
mainThreadModelViewDialog.$setTabDetails(tab1Handle, tab1Details);
mainThreadModelViewDialog.$setTabDetails(tab2Handle, tab2Details);
mainThreadModelViewDialog.$setDialogDetails(dialogHandle, dialogDetails);
});
test('Creating a dialog and calling open on it causes a dialog with correct content and buttons to open', () => {
// If I open the dialog
mainThreadModelViewDialog.$open(dialogHandle);
// Then the opened dialog's content and buttons match what was set
mockDialogService.verify(x => x.showDialog(It.isAny()), Times.once());
assert.notEqual(openedDialog, undefined);
assert.equal(openedDialog.title, dialogDetails.title);
assert.equal(openedDialog.okButton.label, okButtonDetails.label);
assert.equal(openedDialog.okButton.enabled, okButtonDetails.enabled);
assert.equal(openedDialog.cancelButton.label, cancelButtonDetails.label);
assert.equal(openedDialog.cancelButton.enabled, cancelButtonDetails.enabled);
assert.equal(openedDialog.customButtons.length, 2);
assert.equal(openedDialog.customButtons[0].label, button1Details.label);
assert.equal(openedDialog.customButtons[0].enabled, button1Details.enabled);
assert.equal(openedDialog.customButtons[1].label, button2Details.label);
assert.equal(openedDialog.customButtons[1].enabled, button2Details.enabled);
assert.equal(openedDialog.content.length, 2);
assert.equal((openedDialog.content[0] as DialogTab).content, tab1Details.content);
assert.equal((openedDialog.content[0] as DialogTab).title, tab1Details.title);
assert.equal((openedDialog.content[1] as DialogTab).content, tab2Details.content);
assert.equal((openedDialog.content[1] as DialogTab).title, tab2Details.title);
});
test('Button presses are forwarded to the extension host', () => {
// Set up the mock proxy to capture button presses
let pressedHandles = [];
mockExtHostModelViewDialog.setup(x => x.$onButtonClick(It.isAny())).callback(handle => pressedHandles.push(handle));
// Open the dialog so that its buttons can be accessed
mainThreadModelViewDialog.$open(dialogHandle);
// Set up click emitters for each button
let okEmitter = new Emitter<void>();
let cancelEmitter = new Emitter<void>();
let button1Emitter = new Emitter<void>();
let button2Emitter = new Emitter<void>();
openedDialog.okButton.registerClickEvent(okEmitter.event);
openedDialog.cancelButton.registerClickEvent(cancelEmitter.event);
openedDialog.customButtons[0].registerClickEvent(button1Emitter.event);
openedDialog.customButtons[1].registerClickEvent(button2Emitter.event);
// Click the buttons
button1Emitter.fire();
button2Emitter.fire();
okEmitter.fire();
cancelEmitter.fire();
button2Emitter.fire();
cancelEmitter.fire();
button1Emitter.fire();
okEmitter.fire();
// Verify that the correct button click notifications were sent to the proxy
assert.deepEqual(pressedHandles, [button1Handle, button2Handle, okButtonHandle, cancelButtonHandle, button2Handle, cancelButtonHandle, button1Handle, okButtonHandle]);
});
});

View File

@@ -462,11 +462,12 @@ export class IssueReporter extends Disposable {
response.json().then(result => { response.json().then(result => {
this.clearSearchResults(); this.clearSearchResults();
if (result && result.candidates) { // {{SQL CARBON EDIT}}
this.displaySearchResults(result.candidates); // if (result && result.candidates) {
} else { // this.displaySearchResults(result.candidates);
throw new Error('Unexpected response, no candidates property'); // } else {
} // throw new Error('Unexpected response, no candidates property');
// }
}).catch((error) => { }).catch((error) => {
this.logSearchError(error); this.logSearchError(error);
}); });

View File

@@ -59,13 +59,14 @@ export class IssueReporterModel {
assign(this._data, newData); assign(this._data, newData);
} }
// {{SQL CARBON EDIT}}
serialize(): string { serialize(): string {
return ` return `
Issue Type: <b>${this.getIssueTypeTitle()}</b> Issue Type: <b>${this.getIssueTypeTitle()}</b>
${this._data.issueDescription} ${this._data.issueDescription}
VS Code version: ${this._data.versionInfo && this._data.versionInfo.vscodeVersion} SQL Operations Studio version: ${this._data.versionInfo && this._data.versionInfo.vscodeVersion}
OS version: ${this._data.versionInfo && this._data.versionInfo.os} OS version: ${this._data.versionInfo && this._data.versionInfo.os}
${this.getInfos()} ${this.getInfos()}

View File

@@ -23,6 +23,7 @@ suite('IssueReporter', () => {
}); });
}); });
// {{SQL CARBON EDIT}}
test('serializes model skeleton when no data is provided', () => { test('serializes model skeleton when no data is provided', () => {
const issueReporterModel = new IssueReporterModel(); const issueReporterModel = new IssueReporterModel();
assert.equal(issueReporterModel.serialize(), assert.equal(issueReporterModel.serialize(),
@@ -31,7 +32,7 @@ Issue Type: <b>Feature Request</b>
undefined undefined
VS Code version: undefined SQL Operations Studio version: undefined
OS version: undefined OS version: undefined

View File

@@ -435,7 +435,9 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
return this.queryGallery(query).then(({ galleryExtensions, total }) => { return this.queryGallery(query).then(({ galleryExtensions, total }) => {
const extensions = galleryExtensions.map((e, index) => toExtension(e, this.extensionsGalleryUrl, index, query, options.source)); const extensions = galleryExtensions.map((e, index) => toExtension(e, this.extensionsGalleryUrl, index, query, options.source));
const pageSize = query.pageSize;
// {{SQL CARBON EDIT}}
const pageSize = extensions.length;
const getPage = (pageIndex: number) => { const getPage = (pageIndex: number) => {
const nextPageQuery = query.withPage(pageIndex + 1); const nextPageQuery = query.withPage(pageIndex + 1);
return this.queryGallery(nextPageQuery) return this.queryGallery(nextPageQuery)

View File

@@ -31,7 +31,18 @@ function getClient(aiKey: string): typeof appInsights.client {
const client = appInsights.getClient(aiKey); const client = appInsights.getClient(aiKey);
client.channel.setOfflineMode(true); client.channel.setOfflineMode(true);
client.context.tags[client.context.keys.deviceMachineName] = ''; //prevent App Insights from reporting machine name
// {{SQL CARBON EDIT}}
// clear all ID fields from telemetry
client.context.tags[client.context.keys.deviceMachineName] = '';
client.context.tags[client.context.keys.cloudRoleInstance] = '';
// set envelope flags to suppress Vortex ingest header
client.addTelemetryProcessor((envelope, contextObjects) => {
envelope.flags = 0x200000;
return true;
});
if (aiKey.indexOf('AIF-') === 0) { if (aiKey.indexOf('AIF-') === 0) {
client.config.endpointUrl = 'https://vortex.data.microsoft.com/collect/v1'; client.config.endpointUrl = 'https://vortex.data.microsoft.com/collect/v1';
} }

View File

@@ -14,10 +14,15 @@ import product from 'vs/platform/node/product';
export function resolveCommonProperties(commit: string, version: string, machineId: string, installSourcePath: string): TPromise<{ [name: string]: string; }> { export function resolveCommonProperties(commit: string, version: string, machineId: string, installSourcePath: string): TPromise<{ [name: string]: string; }> {
const result: { [name: string]: string; } = Object.create(null); const result: { [name: string]: string; } = Object.create(null);
// {{SQL CARBON EDIT}}
// __GDPR__COMMON__ "common.machineId" : { "endPoint": "MacAddressHash", "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight" } // __GDPR__COMMON__ "common.machineId" : { "endPoint": "MacAddressHash", "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight" }
result['common.machineId'] = machineId; // result['common.machineId'] = machineId;
// __GDPR__COMMON__ "sessionID" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } result['common.machineId'] = '';
result['sessionID'] = uuid.generateUuid() + Date.now(); // // __GDPR__COMMON__ "sessionID" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
// result['sessionID'] = uuid.generateUuid() + Date.now();
result['sessionID'] = '';
// __GDPR__COMMON__ "commitHash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } // __GDPR__COMMON__ "commitHash" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
result['commitHash'] = commit; result['commitHash'] = commit;
// __GDPR__COMMON__ "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } // __GDPR__COMMON__ "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
@@ -32,7 +37,7 @@ export function resolveCommonProperties(commit: string, version: string, machine
result['common.nodePlatform'] = process.platform; result['common.nodePlatform'] = process.platform;
// __GDPR__COMMON__ "common.nodeArch" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } // __GDPR__COMMON__ "common.nodeArch" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
result['common.nodeArch'] = process.arch; result['common.nodeArch'] = process.arch;
// {{SQL CARBON EDIT}} // {{SQL CARBON EDIT}}
result['common.application.name'] = product.nameLong; result['common.application.name'] = product.nameLong;

View File

@@ -20,48 +20,34 @@ export function resolveWorkbenchCommonProperties(storageService: IStorageService
result['common.version.renderer'] = process.versions && (<any>process).versions['chrome']; result['common.version.renderer'] = process.versions && (<any>process).versions['chrome'];
// {{SQL CARBON EDIT}} // {{SQL CARBON EDIT}}
result['common.application.name'] = product.nameLong; result['common.application.name'] = product.nameLong;
getUserId(storageService).then(value => result['common.userId'] = value); // {{SQL CARBON EDIT}}
result['common.userId'] = '';
const lastSessionDate = storageService.get('telemetry.lastSessionDate'); // {{SQL CARBON EDIT}}
const firstSessionDate = storageService.get('telemetry.firstSessionDate') || new Date().toUTCString(); // const lastSessionDate = storageService.get('telemetry.lastSessionDate');
storageService.store('telemetry.firstSessionDate', firstSessionDate); // const firstSessionDate = storageService.get('telemetry.firstSessionDate') || new Date().toUTCString();
storageService.store('telemetry.lastSessionDate', new Date().toUTCString()); // storageService.store('telemetry.firstSessionDate', firstSessionDate);
// storageService.store('telemetry.lastSessionDate', new Date().toUTCString());
// __GDPR__COMMON__ "common.firstSessionDate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } // // __GDPR__COMMON__ "common.firstSessionDate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
result['common.firstSessionDate'] = firstSessionDate; // result['common.firstSessionDate'] = firstSessionDate;
// __GDPR__COMMON__ "common.lastSessionDate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } // // __GDPR__COMMON__ "common.lastSessionDate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
result['common.lastSessionDate'] = lastSessionDate; // result['common.lastSessionDate'] = lastSessionDate;
// __GDPR__COMMON__ "common.isNewSession" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } // // __GDPR__COMMON__ "common.isNewSession" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
result['common.isNewSession'] = !lastSessionDate ? '1' : '0'; // result['common.isNewSession'] = !lastSessionDate ? '1' : '0';
// {{SQL CARBON EDIT}}
// __GDPR__COMMON__ "common.instanceId" : { "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight" } // __GDPR__COMMON__ "common.instanceId" : { "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight" }
result['common.instanceId'] = getOrCreateInstanceId(storageService); // result['common.instanceId'] = getOrCreateInstanceId(storageService);
result['common.instanceId'] = '';
return result; return result;
}); });
} }
function getOrCreateInstanceId(storageService: IStorageService): string {
const result = storageService.get('telemetry.instanceId') || uuid.generateUuid();
storageService.store('telemetry.instanceId', result);
return result;
}
// {{SQL CARBON EDIT}} // {{SQL CARBON EDIT}}
// Get the unique ID for the current user // function getOrCreateInstanceId(storageService: IStorageService): string {
function getUserId(storageService: IStorageService): Promise<string> { // const result = storageService.get('telemetry.instanceId') || uuid.generateUuid();
var userId = storageService.get('common.userId'); // storageService.store('telemetry.instanceId', result);
return new Promise<string>(resolve => { // return result;
// Generate the user id if it has not been created already // }
if (typeof userId === 'undefined') {
let id = Utils.generateUserId();
id.then( newId => {
userId = newId;
resolve(userId);
//store the user Id in the storage service
storageService.store('common.userId', userId);
});
} else {
resolve(userId);
}
});
}

View File

@@ -32,58 +32,59 @@ suite('Telemetry - common properties', function () {
del(parentDir, os.tmpdir(), done); del(parentDir, os.tmpdir(), done);
}); });
test('default', function () { // {{SQL CARBON EDIT}}
return mkdirp(parentDir).then(() => { // test('default', function () {
fs.writeFileSync(installSource, 'my.install.source'); // return mkdirp(parentDir).then(() => {
// fs.writeFileSync(installSource, 'my.install.source');
return resolveWorkbenchCommonProperties(storageService, commit, version, 'someMachineId', installSource).then(props => { // return resolveWorkbenchCommonProperties(storageService, commit, version, 'someMachineId', installSource).then(props => {
assert.ok('commitHash' in props); // assert.ok('commitHash' in props);
assert.ok('sessionID' in props); // assert.ok('sessionID' in props);
assert.ok('timestamp' in props); // assert.ok('timestamp' in props);
assert.ok('common.platform' in props); // assert.ok('common.platform' in props);
assert.ok('common.nodePlatform' in props); // assert.ok('common.nodePlatform' in props);
assert.ok('common.nodeArch' in props); // assert.ok('common.nodeArch' in props);
assert.ok('common.timesincesessionstart' in props); // assert.ok('common.timesincesessionstart' in props);
assert.ok('common.sequence' in props); // assert.ok('common.sequence' in props);
// assert.ok('common.version.shell' in first.data); // only when running on electron // // assert.ok('common.version.shell' in first.data); // only when running on electron
// assert.ok('common.version.renderer' in first.data); // // assert.ok('common.version.renderer' in first.data);
assert.ok('common.osVersion' in props, 'osVersion'); // assert.ok('common.osVersion' in props, 'osVersion');
assert.ok('common.platformVersion' in props, 'platformVersion'); // assert.ok('common.platformVersion' in props, 'platformVersion');
assert.ok('version' in props); // assert.ok('version' in props);
assert.equal(props['common.source'], 'my.install.source'); // assert.equal(props['common.source'], 'my.install.source');
// {{SQL CARBON EDIT}} // // {{SQL CARBON EDIT}}
assert.ok('common.application.name' in props); // assert.ok('common.application.name' in props);
assert.ok('common.firstSessionDate' in props, 'firstSessionDate'); // assert.ok('common.firstSessionDate' in props, 'firstSessionDate');
assert.ok('common.lastSessionDate' in props, 'lastSessionDate'); // conditional, see below, 'lastSessionDate'ow // assert.ok('common.lastSessionDate' in props, 'lastSessionDate'); // conditional, see below, 'lastSessionDate'ow
assert.ok('common.isNewSession' in props, 'isNewSession'); // assert.ok('common.isNewSession' in props, 'isNewSession');
// machine id et al // // machine id et al
assert.ok('common.instanceId' in props, 'instanceId'); // assert.ok('common.instanceId' in props, 'instanceId');
assert.ok('common.machineId' in props, 'machineId'); // assert.ok('common.machineId' in props, 'machineId');
fs.unlinkSync(installSource); // fs.unlinkSync(installSource);
return resolveWorkbenchCommonProperties(storageService, commit, version, 'someMachineId', installSource).then(props => { // return resolveWorkbenchCommonProperties(storageService, commit, version, 'someMachineId', installSource).then(props => {
assert.ok(!('common.source' in props)); // assert.ok(!('common.source' in props));
}); // });
}); // });
}); // });
}); // });
test('lastSessionDate when aviablale', function () { // test('lastSessionDate when aviablale', function () {
storageService.store('telemetry.lastSessionDate', new Date().toUTCString()); // storageService.store('telemetry.lastSessionDate', new Date().toUTCString());
return resolveWorkbenchCommonProperties(storageService, commit, version, 'someMachineId', installSource).then(props => { // return resolveWorkbenchCommonProperties(storageService, commit, version, 'someMachineId', installSource).then(props => {
assert.ok('common.lastSessionDate' in props); // conditional, see below // assert.ok('common.lastSessionDate' in props); // conditional, see below
assert.ok('common.isNewSession' in props); // assert.ok('common.isNewSession' in props);
assert.equal(props['common.isNewSession'], 0); // assert.equal(props['common.isNewSession'], 0);
}); // });
}); // });
test('values chance on ask', function () { test('values chance on ask', function () {
return resolveWorkbenchCommonProperties(storageService, commit, version, 'someMachineId', installSource).then(props => { return resolveWorkbenchCommonProperties(storageService, commit, version, 'someMachineId', installSource).then(props => {

View File

@@ -177,7 +177,8 @@ export class ExtensionsViewlet extends PersistentViewsViewlet implements IExtens
private createDefaultRecommendedExtensionsListViewDescriptor(): IViewDescriptor { private createDefaultRecommendedExtensionsListViewDescriptor(): IViewDescriptor {
return { return {
id: 'extensions.recommendedList', id: 'extensions.recommendedList',
name: localize('recommendedExtensions', "Recommended"), // {{ SQL CARBON EDIT}}
name: localize('recommendedExtensions', "Marketplace"),
location: ViewLocation.Extensions, location: ViewLocation.Extensions,
ctor: RecommendedExtensionsView, ctor: RecommendedExtensionsView,
when: ContextKeyExpr.and(ContextKeyExpr.not('searchExtensions'), ContextKeyExpr.has('defaultRecommendedExtensions')), when: ContextKeyExpr.and(ContextKeyExpr.not('searchExtensions'), ContextKeyExpr.has('defaultRecommendedExtensions')),

View File

@@ -234,6 +234,9 @@ export class ExtensionsListView extends ViewsViewletPanel {
return this.getAllRecommendationsModel(query, options); return this.getAllRecommendationsModel(query, options);
} else if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) { } else if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) {
return this.getRecommendationsModel(query, options); return this.getRecommendationsModel(query, options);
// {{SQL CARBON EDIT}}
} else if (ExtensionsListView.isAllMarketplaceExtensionsQuery(query.value)) {
return this.getAllMarketplaceModel(query, options);
} }
let text = query.value; let text = query.value;
@@ -363,6 +366,42 @@ export class ExtensionsListView extends ViewsViewletPanel {
}); });
} }
// {{SQL CARBON EDIT}}
private getAllMarketplaceModel(query: Query, options: IQueryOptions): TPromise<IPagedModel<IExtension>> {
const value = query.value.trim().toLowerCase();
return this.extensionsWorkbenchService.queryLocal()
.then(result => result.filter(e => e.type === LocalExtensionType.User))
.then(local => {
return this.tipsService.getOtherRecommendations().then((recommmended) => {
const installedExtensions = local.map(x => `${x.publisher}.${x.name}`);
options = assign(options, { text: value, source: 'searchText' });
return TPromise.as(this.extensionsWorkbenchService.queryGallery(options).then((pager) => {
// filter out installed extensions
pager.firstPage = pager.firstPage.filter((p) => {
return installedExtensions.indexOf(`${p.publisher}.${p.name}`) === -1;
});
// sort the marketplace extensions
pager.firstPage.sort((a, b) => {
let isRecommendedA: boolean = recommmended.indexOf(`${a.publisher}.${a.name}`) > -1;
let isRecommendedB: boolean = recommmended.indexOf(`${b.publisher}.${b.name}`) > -1;
// sort recommeded extensions before other extensions
if (isRecommendedA !== isRecommendedB) {
return (isRecommendedA && !isRecommendedB) ? -1 : 1;
}
// otherwise sort by name
return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1;
});
pager.total = pager.firstPage.length;
pager.pageSize = pager.firstPage.length;
return new PagedModel(pager || []);
}));
});
});
}
// Given all recommendations, trims and returns recommendations in the relevant order after filtering out installed extensions // Given all recommendations, trims and returns recommendations in the relevant order after filtering out installed extensions
private getTrimmedRecommendations(installedExtensions: string[], value: string, fileBasedRecommendations: string[], otherRecommendations: string[], workpsaceRecommendations: string[], ) { private getTrimmedRecommendations(installedExtensions: string[], value: string, fileBasedRecommendations: string[], otherRecommendations: string[], workpsaceRecommendations: string[], ) {
const totalCount = 8; const totalCount = 8;
@@ -524,6 +563,11 @@ export class ExtensionsListView extends ViewsViewletPanel {
static isKeymapsRecommendedExtensionsQuery(query: string): boolean { static isKeymapsRecommendedExtensionsQuery(query: string): boolean {
return /@recommended:keymaps/i.test(query); return /@recommended:keymaps/i.test(query);
} }
// {{SQL CARBON EDIT}}
static isAllMarketplaceExtensionsQuery(query: string): boolean {
return /@allmarketplace/i.test(query);
}
} }
export class InstalledExtensionsView extends ExtensionsListView { export class InstalledExtensionsView extends ExtensionsListView {
@@ -560,7 +604,8 @@ export class BuiltInExtensionsView extends ExtensionsListView {
export class RecommendedExtensionsView extends ExtensionsListView { export class RecommendedExtensionsView extends ExtensionsListView {
async show(query: string): TPromise<IPagedModel<IExtension>> { async show(query: string): TPromise<IPagedModel<IExtension>> {
return super.show(!query.trim() ? '@recommended:all' : '@recommended'); // {{SQL CARBON EDIT}}
return super.show('@allmarketplace');
} }
} }