Compare commits

...

147 Commits

Author SHA1 Message Date
Karl Burtram
7553f799e1 Update STS to 4.2.1.6 for async revert (#20494) 2022-08-29 16:46:09 -07:00
Karl Burtram
d64c0549df Bump version for hotfix (#20495) 2022-08-29 16:15:50 -07:00
erpett
e221c9f421 Post release changelog update (#20458) (#20460) 2022-08-24 12:42:29 -07:00
Cheena Malhotra
9fdb2161d4 Update STS version to port SqlClient library update (#20406) 2022-08-18 11:53:03 -07:00
Barbara Valdez
b2ca229e60 Fix unpin notebook (#20393) (#20397)
* convert uri to vscode uri
2022-08-18 08:59:08 -07:00
Vasu Bhog
60037222a0 Ensure SQL Nuget package reference is always updating to latest (#20390) (#20400)
* seperate nuget package references to always update to latest

* nit

* nit comments
2022-08-17 20:43:59 -07:00
Hai Cao
106bb5ecdf update STS in release to port table designer fix (#20394) 2022-08-17 15:26:51 -07:00
Karl Burtram
1bcbb93301 Replace Job Step retry placeholders with values (#20376) (#20388) 2022-08-17 13:21:40 -07:00
Kim Santiago
4fd2d9e76b update STS to 4.2.1.3 (#20386) 2022-08-17 10:59:06 -07:00
Alan Ren
b66031bf16 vbump sts (#20375) 2022-08-16 16:38:39 -07:00
Raymond Truong
e001cb1da3 Vbump sqltoolsservice in mssql config (#20372) 2022-08-16 16:17:12 -07:00
Vasu Bhog
d70c9f2fa7 Add output channel for SQL Bindings extension (#20336) (#20350)
* add output channel for SQL Bindings extension

* add output channel to open once added
2022-08-16 12:18:40 -07:00
Alex Ma
ebdb1783cc [Loc] last minute update to localized XLFs and i18n files (#20342) 2022-08-15 09:26:25 -07:00
Alex Hsu
e0e6b33610 Juno: check in to lego/hb_04604851-bac4-4681-9f74-73de611d6e48_20220814154448821. (#20341) 2022-08-15 08:53:14 -07:00
Sakshi Sharma
cd1618798e Save db project information when saved in scmp file (#20335)
* Save db project information when saved in scmp file

* Address comments
2022-08-15 08:23:32 -07:00
Alex Ma
ef02a8afae json and xlf update for 8-13-2022 (#20340) 2022-08-13 18:05:56 -07:00
Alex Hsu
8f12e001eb Juno: check in to lego/hb_04604851-bac4-4681-9f74-73de611d6e48_20220813153636632. (#20339) 2022-08-13 17:45:44 -07:00
Alan Ren
e6af8ef531 vbump sts (#20338) 2022-08-12 20:01:18 -07:00
Alex Ma
e017675a1c [Loc] addition of heading text (#20337) 2022-08-12 16:59:10 -07:00
Chris LaFreniere
6c596d8495 Skip failing smoke tests (#20331) 2022-08-12 15:18:27 -07:00
Barbara Valdez
2119c96c88 Update pin/unpin icon after pinning notebook (#20273) 2022-08-12 14:59:07 -07:00
Kim Santiago
4971f1bd1a update STS to 4.2.0.15 (#20329) 2022-08-12 14:35:56 -07:00
Sai Avishkar Sreerama
7e99ea8618 Adding a loader spinner before the options load (#20314)
* Adding a loader spinner before the options load

* removing the local var and using const

* Reducing delay between button click and loader visibility,
2022-08-12 16:30:20 -05:00
Cory Rivera
ff05a4e25d Use a "Text Size" label for heading dropdown. (#20319) 2022-08-12 14:17:40 -07:00
Alex Ma
e1952b8d12 Langpack source file update for August (#20328)
* [Loc] Update to August langpack XLF files

* [Loc] Update for langpack source files

* xlf update 8-12-2022

* added update to json files
2022-08-12 12:31:44 -07:00
Alex Hsu
57234a52fd Juno: check in to lego/hb_04604851-bac4-4681-9f74-73de611d6e48_20220812154545836. (#20326) 2022-08-12 09:40:21 -07:00
Alan Ren
c663493690 properly handle copy request (#20320) 2022-08-11 21:22:19 -07:00
Alex Ma
a3acae4777 [Loc] update to sql and sql-migration xlfs (#20318) 2022-08-11 16:28:22 -07:00
Kim Santiago
f6d2af58af Add image tag matching target platform for publish to docker (#20296)
* add the image tag for the sql server version

* add preview text and test

* cleanup
2022-08-11 14:58:09 -07:00
Alex Hsu
b531958402 Juno: check in to lego/hb_04604851-bac4-4681-9f74-73de611d6e48_20220811153733711. (#20311) 2022-08-11 12:41:53 -07:00
Aasim Khan
a7a337f063 Adding properties grid enhancements in execution plan (#20208)
* init push

* Fixing properties in plan comparison

* Add long Text Cell viewer

* Disabling auto edit by default

* Removing text editor
2022-08-11 11:22:12 -07:00
Alan Ren
9ec68087ac allow database name to be empty (#20221)
* allow database to be empty

* test changes

* fix import

* fix test cases

* comment
2022-08-11 10:26:55 -07:00
Sai Avishkar Sreerama
0e05c32f15 Fix for table component checkbox selection focus the previous active … (#20301)
* Fix for table component checkbox selection focus the previous active row instead of newly checked row

* comment updated
2022-08-11 11:47:00 -05:00
Candice Ye
4d91a32bed SQL MIAA list now accounts for new text output from Azure CLI (#20305)
* SQL MIAA list now accounts for new text output in line 1

* Version bump

Co-authored-by: Candice Ye <canye@microsoft.com>
2022-08-10 22:46:55 -07:00
AkshayMata
f73cf78001 Add aria labels (#20265)
Co-authored-by: Akshay Mata <akma@microsoft.com>
2022-08-10 19:58:33 -07:00
Alex Ma
5f928a5218 [Loc] added sql query execution lines (#20303) 2022-08-10 16:53:03 -07:00
Alan Ren
a39d73da24 install another version is not supported (#20300) 2022-08-10 15:36:59 -07:00
Alan Ren
b6cf5b2af0 set the default value to false (#20302) 2022-08-10 15:32:24 -07:00
Cory Rivera
315295710d Temporarily disable cell toolbar smoke tests (#20299) 2022-08-10 14:43:41 -07:00
Barbara Valdez
db8bd021be update relative path to katex css (#20293) 2022-08-10 13:17:48 -07:00
Candice Ye
bb8c02a01b Reverted placeholder value reversal (#20294)
Co-authored-by: Candice Ye <canye@microsoft.com>
2022-08-10 13:08:03 -07:00
Jordan Hays
315e49b2ed update database ledger icon to correct width (#20288) 2022-08-10 12:07:22 -07:00
Kim Santiago
c27c5334ba update container eula link (#20279) 2022-08-10 11:59:42 -07:00
Alex Hsu
f48fa4785d Juno: check in to lego/hb_04604851-bac4-4681-9f74-73de611d6e48_20220810153751933. (#20291) 2022-08-10 09:25:40 -07:00
Alan Ren
2d14665208 announce query execution events (#20285) 2022-08-09 20:23:17 -07:00
Alex Ma
0550d58579 [Loc] small change to sql color value (#20287) 2022-08-09 16:29:55 -07:00
Candice Ye
7a6168d9e6 Fixes to SQL MIAA Compute + Storage page (#20272)
* Max for syncsecondary. Fix syncsecondary update.

* Update the value not the placeholder for SQL MIAA

* Disable save toolbar icon after save/update complete

Co-authored-by: Candice Ye <canye@microsoft.com>
2022-08-08 18:24:09 -07:00
Alan Ren
973c1c2437 show server group color name (#20271) 2022-08-08 17:06:13 -07:00
Kim Santiago
57ef5721a3 vbump schema compare and sql database projects (#20268) 2022-08-08 16:07:21 -07:00
Alex Ma
4e8af635c6 [Loc] first update to xlfs for 8-8-2022 (#20270) 2022-08-08 14:52:48 -07:00
Candice Ye
7564aee5b6 Bumped azure cli and arc ext to 1.5.0 in package.json (#20263)
Co-authored-by: Candice Ye <canye@microsoft.com>
2022-08-08 13:20:41 -07:00
Candice Ye
ad4a16ca82 Config profile is now Kubernetes configuration template in Arc DC Create Wizard (#20261)
* Re-enabled config for direct mode

* Changed name and placement

* Shortened description

* Remove pg and sql as categories for DC

Co-authored-by: Candice Ye <canye@microsoft.com>
2022-08-08 13:20:05 -07:00
Sai Avishkar Sreerama
54d4098f85 Include Object Types logic refactored and options fetching from DacFx (#20031)
* Include Object Types logic refactored and options fetching from DacFx

* Removed localized object types constants

* Prop name updated and references and tests updated

* updated comments

* STS vBump

* updating the test file to pass the PR Validations
2022-08-08 12:12:59 -05:00
Sai Avishkar Sreerama
2b5d2f0a0b Exclude Object Types Coming from DacFx and tests all working as expected (#20015)
* Include Objects Coming from DacFx and tests all working as expected

* Exclude Object types functionality is working as expected and Unit tests

* more refactor updates

* Updated comments and prop name

* Addressing the coments and code updates accordingly

* Updating according to the comments

* STS vbump

* These changes should be deleted with SC changes, not here

* format fixed
2022-08-08 12:04:24 -05:00
Sai Avishkar Sreerama
66115d8f80 options loading from profile if has profile loaded (#19791)
* options loading from profile if has profile loaded

* Removing profile file unwanted changes

* updating profiles options comment

* STS vBump

* revert STS version from this PR
2022-08-08 11:19:55 -05:00
Vasu Bhog
4ebe4c4547 [SQL-bindings] remove watcher for files (#20250)
* remove watcher for files

* nit
2022-08-05 16:18:03 -07:00
Alan Ren
9dee889808 fix the zoom reset issue (#20254) 2022-08-05 13:48:39 -07:00
Alex Hsu
3381d59cea Juno: check in to lego/hb_04604851-bac4-4681-9f74-73de611d6e48_20220805153740212. (#20253) 2022-08-05 09:55:45 -07:00
Candice Ye
c56a8b3d8b Separate SQL MIAA from SQL Instance tile (#20244)
* Made SQL MIAA a new tile from New Deployment

* Remove MIAA from SQL tile. Fix helptext for MIAA tile.

* Switched positions of sql miaa and pg tiles. Made New Instance have miaa instead of sql tile.

Co-authored-by: Candice Ye <canye@microsoft.com>
2022-08-04 10:32:31 -07:00
Alex Hsu
8a43f14b41 Juno: check in to lego/hb_04604851-bac4-4681-9f74-73de611d6e48_20220804154127610. (#20248) 2022-08-04 10:21:40 -07:00
Aasim Khan
d139559d98 Adding arialive to text (#20242) 2022-08-03 20:25:17 -07:00
Candice Ye
baf52a55ff Bump version for bugbash (#20231)
Co-authored-by: Candice Ye <canye@microsoft.com>
2022-08-03 14:06:34 -07:00
Alex Ma
2f746d0b39 Mac OS product build vm image bump (#20232) 2022-08-03 13:56:56 -07:00
Aasim Khan
1cbe11ad38 Adding ariaLive to assessment in progress (#20237) 2022-08-03 13:46:39 -07:00
Alan Ren
4096037167 fix query results view issue (#20234) 2022-08-03 10:45:33 -07:00
Alan Ren
f72dbccc52 no-op is the object is disposed (#20236) 2022-08-03 10:45:11 -07:00
Cory Rivera
453f8e5525 Set cell preview mode when toggling edit mode. (#20220)
* Also fix leftover settings changes in markdown smoke test.
2022-08-03 09:36:24 -07:00
Alan Ren
4d1be1e288 fix menu item not showing up for non-English languages (#20224)
* new object type property

* vbump sts

* fix azure tree

* mark as optional

* Fix test errors
2022-08-02 13:39:20 -07:00
Candice Ye
5d23627165 Converting all SQL MIAA commands to use K8s API commands (#20203)
* refresh does not differentiate direct vs indirect

* For direct mode MIAA commands, always use --use-k8s

* Changed comment language to use ARM instead of direct and K8S API instead of indirect

Co-authored-by: Candice Ye <canye@microsoft.com>
2022-08-02 11:12:25 -07:00
Alex Ma
8f5b3ef81d [Loc] update to sql xlf (#20227) 2022-08-02 10:02:49 -07:00
Cory Rivera
c37149ccfb Type out query text one letter at a time in autocompletion test. (#20222) 2022-08-01 16:59:28 -07:00
Alex Hsu
74c715f16f Juno: check in to lego/hb_04604851-bac4-4681-9f74-73de611d6e48_20220801153846057. (#20218) 2022-08-01 11:39:32 -07:00
Alex Hsu
efc7789fe4 Juno: check in to lego/hb_04604851-bac4-4681-9f74-73de611d6e48_20220731153825655. (#20216)
Co-authored-by: Alex Ma <alma1@microsoft.com>
2022-08-01 10:46:24 -07:00
Alex Hsu
5e7bc2a05b Juno: check in to lego/hb_04604851-bac4-4681-9f74-73de611d6e48_20220730154003990. (#20215) 2022-08-01 10:41:44 -07:00
Alan Ren
6be1420220 do not run query in multi-selection mode (#20213)
* do not run query in multi-selection mode

* fix test

* fix more tests
2022-07-30 14:29:32 -07:00
Charles Gagnon
6704bc552a vBump query history (#20214) 2022-07-29 19:36:03 -07:00
Alan Ren
67ecd6d3d9 fix the grid goes blank issue (#20209) 2022-07-29 18:12:02 -07:00
Alex Ma
afe1b4392f [Loc] addition to query-history (#20211) 2022-07-29 16:44:19 -07:00
Charles Gagnon
866fa76a65 Add double click action support for query history (#20204)
* Add double click action support for query history

* localize descriptions
2022-07-29 13:14:26 -07:00
Alex Hsu
395bc3f149 Juno: check in to lego/hb_04604851-bac4-4681-9f74-73de611d6e48_20220729153613975. (#20205) 2022-07-29 10:27:38 -07:00
Lewis Sanchez
bf6c503c07 Bumps azdataGraph version to 0.0.38 (#20201) 2022-07-28 17:26:40 -07:00
Alex Ma
ab9e67be30 [Loc] update to arc and sql xlfs (#20200) 2022-07-28 15:15:18 -07:00
erpett
055a0d6c67 fixing indent (#20198) 2022-07-28 14:21:50 -07:00
Lewis Sanchez
9fc251259c Adds Ability to Search for Nodes in Compared Execution Plans (#20168)
* Adds find node button to comparison plans.

* Can search multiple nodes (improve widget UI and initialization)

* Adjusts how second plan is added to the node search widget

* Adds styling to the find node action bar

* Removes unused code

* Minor clean up

* Cleans up CSS redundancy

* Adjusts property names according to access specifiers

* Corrects find node behavior to match SSMS

* Dependency injects instantiation service

* Adds additional property to telemetry event.

* Adds undefined to getter return signatures for plans

* Adds checks around active execution plan properties

* Code review change

* Code review changes
2022-07-28 14:14:32 -07:00
erpett
489f5f359f updating changelog with additional changes (#20197) 2022-07-28 13:52:38 -07:00
Candice Ye
28d5382dc3 Added check for .toLowerCase and added upgrade message for clarity. (#20187)
Co-authored-by: Candice Ye <canye@microsoft.com>
2022-07-28 13:52:26 -07:00
Alan Ren
d3073a33fe vbump sts (#20196) 2022-07-28 13:29:25 -07:00
Charles Gagnon
6b1ef0e2ad Store query history items as data, not tree node (#20195) 2022-07-28 11:13:18 -07:00
Charles Gagnon
06bb31b944 Fix query history container icon (#20194) 2022-07-28 09:58:38 -07:00
Charles Gagnon
466b80fb21 Lint fixes from DefinitelyTyped (#20185) 2022-07-28 08:12:43 -07:00
Alan Ren
bb4b00a25a fix recent closed (#20188) 2022-07-27 18:44:08 -07:00
Vasu Bhog
514b0315cc SQL Bindings add telemetry points (#20125)
* add telemetry points

* address comments
2022-07-27 16:38:37 -07:00
erpett
4411a1f319 updating changelog for 1.38 (#20182) 2022-07-27 11:48:52 -07:00
Candice Ye
bd98f67113 connection to connectivity, .lower on notebook (#20177)
Co-authored-by: Candice Ye <canye@microsoft.com>
2022-07-26 17:37:14 -07:00
Alex Ma
fea7f5156f [Loc] added strings to sql.xlf (#20178) 2022-07-26 16:54:17 -07:00
Candice Ye
79ba314953 Prompt for arcdata install upon extension activation (fix) (#20167)
* Made prompt when no arcdata upon startup

* Changed to NoAzureCLIArcExtError

Co-authored-by: Candice Ye <canye@microsoft.com>
2022-07-26 16:45:30 -07:00
Aasim Khan
518bb33a2f Adding plan tree to execution plan (#20158)
* Adding plan tree

* Fixing localization keys

* Removed whitespace
2022-07-26 14:30:46 -07:00
Alex Ma
441b551c0a [Loc] SQL migration xlf update (#20172) 2022-07-26 10:21:34 -07:00
Alex Hsu
861215c611 Juno: check in to lego/hb_04604851-bac4-4681-9f74-73de611d6e48_20220726153652100. (#20170) 2022-07-26 09:52:55 -07:00
brian-harris
59f96ef2e3 remove unneccessary webpack import (#20166) 2022-07-25 22:08:43 -07:00
Candice Ye
00bacee1da Bump to 1.4.2 for bugbash (#20164)
Co-authored-by: Candice Ye <canye@microsoft.com>
2022-07-25 16:51:27 -07:00
Alan Ren
ddefdac6cc vbump sts (#20159) 2022-07-25 15:53:36 -07:00
Alan Ren
816cd5a997 fix the dropdown menu width logic (#20165)
* fix the dropdown menu width logic

* comment
2022-07-25 15:53:05 -07:00
Alex Hsu
4ea210e794 Juno: check in to lego/hb_04604851-bac4-4681-9f74-73de611d6e48_20220725153813588. (#20162) 2022-07-25 13:41:28 -07:00
Alex Hsu
7da69c82e5 Juno: check in to lego/hb_04604851-bac4-4681-9f74-73de611d6e48_20220724153818370. (#20161) 2022-07-25 13:41:15 -07:00
brian-harris
78b7c3cfd4 Add new tabbed dashboard, monitoring with breadcrumb navigation (#19995)
* SQL DB monitoring and  Dashboard refactor

* Merge remote-tracking branch 'origin/main' into dev/brih/feature/sql-migration-dashboard-tabs

* update filter text and optimize page load

* update migration column order, names and statusbox

* add column table sorting

* add new migration and pipeline status values, etc

* address review feedback
2022-07-25 10:06:17 -07:00
Alex Ma
db39571394 [Loc] change to resource deployment (#20154) 2022-07-22 17:11:43 -07:00
Alan Ren
2f1fbe5473 vbump STS (#20150) 2022-07-22 13:48:42 -07:00
Alan Ren
33ade09608 support sql server 2022 (#20152) 2022-07-22 13:48:34 -07:00
Cory Rivera
c2be6447b5 Prevent cell's edit mode from being incorrectly cleared when a text component is initialized. (#20120) 2022-07-22 10:22:56 -07:00
Alex Hsu
7176629e44 Juno: check in to lego/hb_04604851-bac4-4681-9f74-73de611d6e48_20220722154031353. (#20149) 2022-07-22 09:31:29 -07:00
Jordan Hays
3ebad4e2b7 fix icon path for ledger history table to be recognized as the correct node type (#20144) 2022-07-21 22:20:54 -07:00
Lewis Sanchez
43e7e35df5 Update azdataGraph version to 0.0.37 (#20140)
* Updates azdataGraph version to 0.0.37

* Enables collapse/expand node with highlight resize
2022-07-21 16:15:54 -07:00
Jordan Hays
7b08ecc4cc Dev/nofield/add ledger icons (#20136)
* ledger object icons
2022-07-21 16:02:53 -07:00
Charles Gagnon
1367f29a8a Fix null ref in contributed tree views (#20138) 2022-07-21 12:26:22 -07:00
Kim Santiago
dc7522c661 Revert "Fix sql projects not using OutputPath in sqlproj for publishing (#19987)" (#20127)
This reverts commit 70f0e7264b.
2022-07-21 08:11:23 -07:00
Alan Ren
5334343856 update instructions (#20129) 2022-07-20 16:23:44 -07:00
Charles Gagnon
1ebf9dcc6a Update extension integration test guide (#20126)
* Update extension integration test guide

* cleanup
2022-07-20 15:40:39 -07:00
Kim Santiago
3196e99bd6 fix diff editor colors not getting reversed after vscode merge (#20118) 2022-07-20 14:00:01 -07:00
Maddy
7dd36ae7b4 fix: image disappear issue (#20039)
* check if base64 value is from image tag

* add test

* check image regex

* add comment

* update comment
2022-07-20 13:58:49 -07:00
Candice Ye
239e7af4e6 Changed connetivity mode to have object defaultvalue. Changed connection to connectivity mode. (#20089)
Co-authored-by: Candice Ye <canye@microsoft.com>
2022-07-20 13:04:46 -07:00
Charles Gagnon
ada1588bb7 Fix BDC tree getting stuck loading (#20116) 2022-07-20 12:50:11 -07:00
dependabot[bot]
2d9720962a Bump terser from 4.8.0 to 4.8.1 (#20102)
Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 10:26:19 -07:00
Alex Hsu
6d8c66f535 Juno: check in to lego/hb_04604851-bac4-4681-9f74-73de611d6e48_20220720153955946. (#20108) 2022-07-20 09:25:16 -07:00
Alex Ma
96c52ad883 [Loc] changes made on 7-19-2022 (#20104) 2022-07-20 03:03:52 -07:00
Candice Ye
6c59779137 Updated CategoryValue defaultValue docs (#20101) 2022-07-19 21:04:52 -07:00
Candice Ye
5e88338423 Reverted changes to Radio buttons default value (#20100)
* Reverted resourcedeploy changes

* Replaced the !

Co-authored-by: Candice Ye <canye@microsoft.com>
2022-07-19 21:04:39 -07:00
Alan Ren
3d2f729586 support build pipeline that only produces extension vsix files (#20099)
* support only build extensions

* use ne

* copy extensions to drop
2022-07-19 18:58:29 -07:00
Charles Gagnon
ac80703b75 Add size to aria labels on BDC deployments (#20098)
* Add size to aria labels on BDC deployments

* One more
2022-07-19 16:30:58 -07:00
Karl Burtram
23034cd1bd Fix restore dialog null references (#20096)
* Fix restore dialog null references

* Make database field optional like other properties in viewmodel class
2022-07-19 15:42:34 -07:00
Alan Ren
bd36467b99 new dashboard icon (#20087) 2022-07-19 15:00:26 -07:00
Candice Ye
db3dda8519 Got rid of need for casting (#20088)
Co-authored-by: Candice Ye <canye@microsoft.com>
2022-07-19 14:12:03 -07:00
Charles Gagnon
709d15a392 vBump query history (#20080)
* vBump query history

* 0.3.0
2022-07-19 11:23:15 -07:00
Charles Gagnon
d31e33835c [RC1] vBump query history extension (#20081) 2022-07-19 11:07:56 -07:00
Candice Ye
f321b447f2 Various fixes in Arc extension (#20072)
* location description added

* Made Service Tier dynamically enabled

* Added generalpurpose checks for sql miaa deploy notebook

Co-authored-by: Candice Ye <canye@microsoft.com>
2022-07-19 10:29:32 -07:00
Candice Ye
89dbcb6638 Fixed default value for radio options builder if using resourceType displayName (#20070)
Co-authored-by: Candice Ye <canye@microsoft.com>
2022-07-19 10:29:19 -07:00
Karl Burtram
102f3794f2 Revert "Bring back tar vBump (#20016)" (#20074)
This reverts commit 540c931210.
2022-07-19 10:19:19 -07:00
Alex Hsu
697cf93c65 Juno: check in to lego/hb_04604851-bac4-4681-9f74-73de611d6e48_20220719154044132. (#20076) 2022-07-19 10:13:29 -07:00
Alan Ren
18921fc764 vbump STS (#20073) 2022-07-19 09:53:04 -07:00
Vasu Bhog
9e8b80f12f delete progress notification not needed (#20060) 2022-07-19 09:43:55 -07:00
Candice Ye
bfbb78827f Changed default values for high availability and service tier to accommodate rd fix (#20071)
Co-authored-by: Candice Ye <canye@microsoft.com>
2022-07-18 17:40:40 -07:00
Kim Santiago
d418d7c01b Add collapse project tree setting (#20064)
* Add sql projects setting to start with all project trees collapsed

* cleanup

* update string

* update string again
2022-07-18 17:04:39 -07:00
Candice Ye
77e7a90c20 Make connection mode a displayname (#20068)
* dropdown not supporting dynamic enablement

* paused for weekend

* Make connection mode display with capitalization but value lowercase

* Undo changes in modelviewutils

Co-authored-by: Candice Ye <canye@microsoft.com>
2022-07-18 17:02:31 -07:00
Alex Ma
fae9ccb531 [Loc] arc xlf update (#20069) 2022-07-18 15:21:54 -07:00
Alex Ma
dbd3a271c4 small fix for arc package nls comma (#20066) 2022-07-18 12:32:28 -07:00
erpett
296cf8015a Version Bumping now that the release branch is forked (#20065) 2022-07-18 12:08:39 -07:00
Candice Ye
97288c421e Added help text for data controller (#20057)
Co-authored-by: Candice Ye <canye@microsoft.com>
2022-07-18 12:03:58 -07:00
485 changed files with 28310 additions and 18051 deletions

View File

@@ -1,5 +1,81 @@
# Change Log
## Version 1.39.0
* Release date: August 24, 2022
* Release status: General Availability
## What's new in 1.39.0
* New Features:
* Deployment Wizard - Azure Data Studio now supports SQL Server 2022 (Preview) in the Deployment Wizard for both local and container installation.
* Object Explorer - Added Ledger icons and scripting support to Object Explorer for Ledger objects.
* Dashboard - Added hexadecimal values to support color detection.
* Query Plan Viewer - Added the ability to copy text from cells in the Properties Pane of a query plan.
* Query Plan Viewer - Introduced a "find node" option in plan comparison to search for nodes in either the original or added plan.
* Table Designer - Now supports the ability to add included columns to a nonclustered index, and the ability to create filtered indexes.
* SQL Projects - Publish options were added to the Publish Dialog.
* Query History Extension - Added double-click support for query history to either open the query or immediately execute it, based on user configuration.
* Bug Fixes:
* Dashboard - Fixed an accessibility issue that prevented users from being able to access tooltip information using the keyboard.
* Voiceover - Fixed a bug that caused voiceover errors across the Dashboard, SQL Projects, SQL Import Wizard, and SQL Migration extensions.
* Schema Compare - Fixed a bug that caused the UI to jump back to the top of the options list after selecting/deselecting any option.
* Schema Compare - Fixed a bug involving Schema Compare (.SCMP) file incompatibility with Database Project information causing errors when reading and using information stored in this file type.
* Object Explorer - Fixed a bug that caused menu items in Object Explorer not to show up for non-English languages.
* Table Designer - Fixed a bug that caused the History Table name not to be consistent with the current table name when working with System-Versioned Tables.
* Table Designer - Fixed a bug in the Primary Key settings that caused the "Allow Nulls" option to be checked, but disabled, preventing users from changing this option.
* Query Editor - Fixed a bug that prevented the SQLCMD in T-SQL from working correctly, giving false errors when running scripts in Azure Data Studio.
* Query Editor - Fixed a bug that caused user-specified zoom settings to reset to default when selecting JSON values after query that returned JSON dataset was ran.
* SQL Projects - Fixed a bug that caused the "Generate Script" command to not work correctly when targeting a new Azure SQL Database.
* Notebooks - Fixed a bug that caused pasted images to disappear from editor after going out of edit mode.
* Notebooks - Fixed a bug that caused a console error message to appear after opening a markdown file.
* Notebooks - Fixed a bug that prevented markdown cell toolbar shortcuts from working after creating a new split view cell.
* Notebooks - Fixed a bug that caused text cells to be erroneously created in split view mode when the notebook default text edit mode was set to "Markdown".
| Platform |
| --------------------------------------- |
| [Windows User Installer][win-user] |
| [Windows System Installer][win-system] |
| [Windows ZIP][win-zip] |
| [macOS ZIP][osx-zip] |
| [Linux TAR.GZ][linux-zip] |
| [Linux RPM][linux-rpm] |
| [Linux DEB][linux-deb] |
[win-user]: https://go.microsoft.com/fwlink/?linkid=2198663
[win-system]: https://go.microsoft.com/fwlink/?linkid=2198878
[win-zip]: https://go.microsoft.com/fwlink/?linkid=2198664
[osx-zip]: https://go.microsoft.com/fwlink/?linkid=2198762
[linux-zip]: https://go.microsoft.com/fwlink/?linkid=2198879
[linux-rpm]: https://go.microsoft.com/fwlink/?linkid=2198880
[linux-deb]: https://go.microsoft.com/fwlink/?linkid=2198763
## Version 1.38.0
* Release date: July 27, 2022
* Release status: General Availability
## What's new in 1.38.0
* New Features:
* VS Code merges to 1.62 - This release includes updates to VS Code from the three previous VS Code releases. Read [their release notes](https://code.visualstudio.com/updates/v1_62) to learn more.
* Table Designer - New column added to Table Designer for easier access to additional actions specific to individual rows.
* Query Plan Viewer - The Top Operations pane view now includes clickable links to operations in each of its rows to show the runtime statistics which can be used to evaluate estimated and actual rows when analyzing a plan.
* Query Plan Viewer - Improved UI on selected operation node in the Execution Plan.
* Query Plan Viewer - The keyboard command **CTRL + M** no longer executes queries. It now just enables or disables the actual execution plan creation when a query is executed.
* Query Plan Viewer - Plan labels are now updated in the Properties window when plans are compared and the orientation is toggled from horizontal to vertical, and back.
* Query Plan Viewer - Updates were made to the Command Palette. All execution plan commands are prefixed with "Execution Plan", so that they are easier to find and use.
* Query Plan Viewer - A collapse/expand functionality is now available at the operator level to allow users to hide or display sections of the plan during analysis.
* Query History - The Query History extension was refactored to be fully implemented in an extension. This makes the history view behave like all other extension views and also allows for searching and filtering in the view by selecting the view and typing in your search text.
* Bug Fixes:
* Table Designer - Error found in edit data tab when switching back to previously selected column when adding a new row. To fix this, editing the table is now disabled while new rows are being added and only reenabled afterwards.
* Query Editor - Fixed coloring issues for new T-SQL functions in the Query Editor.
* Query Plan Viewer - Fixed bug that caused custom zoom level spinner to allow values outside valid range.
* Dashboard - Fixed issue that caused incorrect displaying of insight widgets on the dashboard.
* Notebooks - Fixed issue where keyboard shortcuts and toolbar buttons were not working when first creating a Split View markdown cell.
* Notebooks - Fixed issue where cell languages were not being set correctly when opening an ADS .NET Interactive notebook in VS Code.
* Notebooks - Fixed issue where notebook was being opened as empty when exporting a SQL query as a notebook.
* Notebooks - Disables install and uninstall buttons in Manage Packages dialog while a package is being installed or uninstalled.
* Notebooks - Fixed issue where cell toolbar buttons were not refreshing when converting cell type.
* Notebooks - Fixed issue where notebook was not opening if a cell contains an unsupported output type.
* Schema Compare - Fixed issue where views and stored procedures were not correctly recognized by schema compare after applying changes.
## Version 1.37.0
* Release date: June 15, 2022
* Release status: General Availability
@@ -22,26 +98,7 @@
* Schema Compare - Fixed issue with indexes not being added correctly when updating project from database.
* Notebooks - Fixed inconsistencies with notebook cell behavior and toolbars.
* Notebooks - Fixed issues with keyboard navigation.
| Platform |
| --------------------------------------- |
| [Windows User Installer][win-user] |
| [Windows System Installer][win-system] |
| [Windows ZIP][win-zip] |
| [macOS ZIP][osx-zip] |
| [Linux TAR.GZ][linux-zip] |
| [Linux RPM][linux-rpm] |
| [Linux DEB][linux-deb] |
[win-user]: https://go.microsoft.com/fwlink/?linkid=2198663
[win-system]: https://go.microsoft.com/fwlink/?linkid=2198878
[win-zip]: https://go.microsoft.com/fwlink/?linkid=2198664
[osx-zip]: https://go.microsoft.com/fwlink/?linkid=2198762
[linux-zip]: https://go.microsoft.com/fwlink/?linkid=2198879
[linux-rpm]: https://go.microsoft.com/fwlink/?linkid=2198880
[linux-deb]: https://go.microsoft.com/fwlink/?linkid=2198763
## Version 1.36.2
* Release date: May 20, 2022
* Release status: General Availability

View File

@@ -121,7 +121,7 @@ steps:
set -e
DISPLAY=:10 ./scripts/test.sh --build --tfs "Unit Tests" --coverage
displayName: Run unit tests (Electron)
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'))
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'), ne(variables['EXTENSIONS_ONLY'], 'true'))
- script: |
# Figure out the full absolute path of the product we just built
@@ -134,7 +134,7 @@ steps:
VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/azuredatastudio-reh-linux-x64" \
DISPLAY=:10 ./scripts/test-integration.sh --build --tfs "Integration Tests"
displayName: Run integration tests (Electron)
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'))
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'), ne(variables['EXTENSIONS_ONLY'], 'true'))
- script: |
# Figure out the full absolute path of the product we just built
@@ -178,11 +178,13 @@ steps:
set -e
yarn gulp vscode-linux-x64-build-deb
displayName: Build Deb
condition: and(succeeded(), ne(variables['EXTENSIONS_ONLY'], 'true'))
- script: |
set -e
yarn gulp vscode-linux-x64-build-rpm
displayName: Build Rpm
condition: and(succeeded(), ne(variables['EXTENSIONS_ONLY'], 'true'))
- task: UseDotNet@2
displayName: 'Install .NET Core sdk for signing'

View File

@@ -20,7 +20,7 @@ jobs:
- job: macOS
condition: and(succeeded(), eq(variables['VSCODE_BUILD_MACOS'], 'true'), ne(variables['VSCODE_QUALITY'], 'saw'))
pool:
vmImage: 'macOS-10.15'
vmImage: 'macos-latest'
dependsOn:
- Compile
steps:
@@ -30,7 +30,7 @@ jobs:
- job: macOS_Signing
condition: and(succeeded(), eq(variables['VSCODE_BUILD_MACOS'], 'true'), eq(variables['signed'], true), ne(variables['VSCODE_QUALITY'], 'saw'))
pool:
vmImage: 'macOS-10.15'
vmImage: 'macos-latest'
dependsOn:
- macOS
steps:

View File

@@ -403,7 +403,7 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
.withProps({
inputType: 'number',
width: '100%',
placeHolder: '0'
value: '0'
})
.component();
this.retryIntervalBox = view.modelBuilder.inputBox()
@@ -411,7 +411,7 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
.withProps({
inputType: 'number',
width: '100%',
placeHolder: '0'
value: '0'
}).component();
let retryAttemptsContainer = view.modelBuilder.formContainer()

View File

@@ -158,7 +158,7 @@
"metadata": {},
"outputs": [],
"source": [
"is_indirect = arc_data_controller_connectivity_mode == 'Indirect'\n",
"is_indirect = arc_data_controller_connectivity_mode.lower() == 'indirect'\n",
"\n",
"if not is_indirect:\n",
"\trun_command('az login')"

View File

@@ -98,7 +98,8 @@
"source": [
"print (f'Creating the SQL managed instance - Azure Arc instance')\n",
"\n",
"is_indirect = arc_data_controller_connection_mode == 'indirect'\n",
"is_indirect = arc_data_controller_connectivity_mode.lower() == 'indirect'\n",
"is_general_purpose = sql_service_tier == 'GeneralPurpose'\n",
"\n",
"# Indirect Mode Parameters\n",
"retention_days = f' --retention-days \"{sql_retention_days}\"' if is_indirect and sql_retention_days else \"\"\n",
@@ -110,8 +111,11 @@
"cores_limit_option = f' --cores-limit \"{sql_cores_limit}\"' if sql_cores_limit else \"\"\n",
"memory_request_option = f' --memory-request \"{sql_memory_request}Gi\"' if sql_memory_request else \"\"\n",
"memory_limit_option = f' --memory-limit \"{sql_memory_limit}Gi\"' if sql_memory_limit else \"\"\n",
"readable_secondaries = f' --readable-secondaries \"{sql_readable_secondaries}\"' if sql_readable_secondaries else \"\"\n",
"sync_secondary_to_commit = f' --sync-secondary-to-commit \"{sql_sync_secondary_to_commit}\"' if sql_sync_secondary_to_commit else \"\"\n",
"\n",
"sql_replicas_option = f' --replicas {sql_replicas}' if sql_replicas and not is_general_purpose else \"\"\n",
"readable_secondaries = f' --readable-secondaries \"{sql_readable_secondaries}\"' if sql_readable_secondaries and not is_general_purpose else \"\"\n",
"sync_secondary_to_commit = f' --sync-secondary-to-commit \"{sql_sync_secondary_to_commit}\"' if sql_sync_secondary_to_commit and not is_general_purpose else \"\"\n",
"\n",
"\n",
"storage_class_data_option = f' --storage-class-data \"{sql_storage_class_data}\"'if sql_storage_class_data else \"\"\n",
"storage_class_datalogs_option = f' --storage-class-datalogs \"{sql_storage_class_datalogs}\"'if sql_storage_class_datalogs else \"\"\n",
@@ -130,7 +134,7 @@
"\n",
"os.environ[\"AZDATA_USERNAME\"] = sql_username\n",
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_SQL_PASSWORD\"]\n",
"cmd = f'az sql mi-arc create --name {sql_instance_name}{namespace} --replicas {sql_replicas}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}{readable_secondaries}{sync_secondary_to_commit}{storage_class_data_option}{storage_class_datalogs_option}{storage_class_logs_option}{storage_class_backup_option}{volume_size_data}{volume_size_datalogs}{volume_size_logs}{volume_size_backups}{retention_days}{service_tier}{dev_use}{license_type}{cores_limit}{use_k8s}'\n",
"cmd = f'az sql mi-arc create --name {sql_instance_name}{namespace}{sql_replicas_option}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}{readable_secondaries}{sync_secondary_to_commit}{storage_class_data_option}{storage_class_datalogs_option}{storage_class_logs_option}{storage_class_backup_option}{volume_size_data}{volume_size_datalogs}{volume_size_logs}{volume_size_backups}{retention_days}{service_tier}{dev_use}{license_type}{cores_limit}{use_k8s}'\n",
"out=run_command()"
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -20,7 +20,8 @@
"arc.data.controller.select.cluster.title": "Select from existing Kubernetes clusters",
"arc.data.controller.kube.cluster.context": "Cluster context",
"arc.data.controller.cluster.config.profile.title": "Choose the config profile",
"arc.data.controller.cluster.config.profile": "Config profile",
"arc.data.controller.cluster.config.profile": "Kubernetes configuration template",
"arc.data.controller.cluster.config.profile.description": "Choose the Kubernetes configuration template that matches with your Kubernetes distribution. This template provides defaults for storage class, service type, etc.",
"arc.data.controller.cluster.config.profile.loading": "Loading config profiles",
"arc.data.controller.cluster.config.profile.loadingcompleted": "Loading config profiles complete",
"arc.data.controller.create.azureconfig.title": "Azure Configuration",
@@ -28,7 +29,9 @@
"arc.data.controller.project.details.title": "Azure details",
"arc.data.controller.project.details.description": "Select the subscription to manage deployed resources and costs. Use resource groups like folders to organize and manage all your resources.",
"arc.data.controller.details.title": "Data controller details",
"arc.data.controller.details.description": "For indirect mode, provide a namespace, name and storage class for your Azure Arc data controller. This name will be used to identify your Arc instance for remote management and monitoring. For direct mode you do not need to provide a namespace, but please provide the custom location name.",
"arc.data.controller.details.description": "For indirect mode, namespace, name and storage class are required. This name will be used to identify your Arc instance for remote management and monitoring. For direct mode, custom location name is required.",
"arc.data.controller.indirect.display.name": "Indirect",
"arc.data.controller.direct.display.name": "Direct",
"arc.data.controller.connectivity.mode": "Connectivity mode",
"arc.data.controller.namespace": "Data controller namespace",
"arc.data.controller.namespace.description": "Data controller namespace. Indirect mode only.",
@@ -36,6 +39,7 @@
"arc.data.controller.name": "Data controller name",
"arc.data.controller.name.validation.description": "Name must consist of lower case alphanumeric characters, '-' or '.', start/end with an alphanumeric character and be 253 characters or less in length.",
"arc.data.controller.location": "Location",
"arc.data.controller.location.description": "Location is only used for indirect mode deployment. This field will be ignored for direct mode.",
"arc.data.controller.infrastructure": "Infrastructure",
"arc.data.controller.custom.location": "Custom Location",
"arc.data.controller.custom.location.description": "The name of the custom location. Direct mode only.",
@@ -234,6 +238,8 @@
"arc.agreement.sql.help.text.learn.more": "Learn more",
"arc.agreement.sql.help.text.learn.more.ariaLabel": "Learn more about Azure Arc enabled Managed Instance",
"arc.sql.pitr.retention.description": "Configure retention for point-in-time backups. {0}",
"arc.data.controller.help.text": "The Kubernetes cluster must already be arc-enabled with the az connectedk8s connect command. Please use our {0} to learn more.",
"arc.data.controller.help.text.documentation.page": "documentation page",
"arc.agreement.sql.help.text.terms.of.use": "Terms of use",
"arc.agreement.sql.help.text.privacy.policy": "Privacy policy",
"arc.agreement.sql.help.text.azure.marketplace.terms": "Azure Marketplace Terms"

View File

@@ -371,3 +371,15 @@ export function getTimeStamp(dateTime: string | undefined): number {
export function checkISOTimeString(dateTime: string): boolean {
return /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d.*Z/.test(dateTime);
}
/**
* Parses out the SQL MIAA list from the raw json output
* @param raw The raw version output from az sql mi-arc list
*/
export function parseMiaaList(raw: string): string | undefined {
// The output of az sql mi-arc list looks like:
// 'Found 1 Arc-enabled SQL Managed Instances in namespace testns1\r\n[\r\n {\r\n "name": "sqlinstance1",\r\n "primaryEndpoint": "20.236.10.81,1422",\r\n "replicas": "3/3",\r\n "state": "Ready"\r\n }\r\n]'
const lines = raw.split('\n');
lines.splice(0, 1);
return lines.join('\n');
}

View File

@@ -253,6 +253,10 @@ export function deletingInstance(name: string): string { return localize('arc.de
export function installingExtension(name: string): string { return localize('arc.installingExtension', "Installing extension '{0}'...", name); }
export function extensionInstalled(name: string): string { return localize('arc.extensionInstalled', "Extension '{0}' has been installed.", name); }
export function updatingInstance(name: string): string { return localize('arc.updatingInstance', "Updating instance '{0}'...", name); }
export function upgradingDirectDC(name: string, desiredVersion: string, resourceGroup: string): string { return localize('arc.upgradingDirectDC', "Upgrading data controller '{0}' with command 'az arcdata dc upgrade --desired-version {1} --name {0} --resource-group {2}'", name, desiredVersion, resourceGroup); }
export function upgradingIndirectDC(name: string, desiredVersion: string, namespace: string): string { return localize('arc.upgradingIndirectDC', "Upgrading data controller '{0}' with command 'az arcdata dc upgrade --desired-version {1} --name {0} --k8s-namespace {2} --use-k8s'", name, desiredVersion, namespace); }
export function upgradingDirectMiaa(name: string, resourceGroup: string): string { return localize('arc.upgradingDirectMiaa', "Upgrading SQL MIAA '{0}' with command 'az sql mi-arc upgrade --name {0} --resource-group {1}'", name, resourceGroup); }
export function upgradingIndirectMiaa(name: string, namespace: string): string { return localize('arc.upgradingIndirectMiaa', "Upgrading SQL MIAA '{0}' with command 'az sql mi-arc upgrade --name {0} --k8s-namespace {1} --use-k8s'", name, namespace); }
export function instanceDeleted(name: string): string { return localize('arc.instanceDeleted', "Instance '{0}' deleted", name); }
export function instanceUpdated(name: string): string { return localize('arc.instanceUpdated', "Instance '{0}' updated", name); }
export function extensionsDropped(name: string): string { return localize('arc.extensionsDropped', "Extensions '{0}' dropped", name); }

View File

@@ -6,7 +6,7 @@
import { ControllerInfo, ResourceType } from 'arc';
import * as azExt from 'az-ext';
import * as vscode from 'vscode';
import { ConnectionMode } from '../constants';
import { parseMiaaList } from '../common/utils';
import * as loc from '../localizedConstants';
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
@@ -69,15 +69,6 @@ export class ControllerModel {
}
public async refresh(showErrors: boolean = true, namespace: string): Promise<void> {
await this.refreshController(showErrors, namespace);
if (this._controllerConfig?.spec.settings.azure.connectionMode === ConnectionMode.direct) {
await this.refreshDirectMode(this._controllerConfig?.spec.settings.azure.resourceGroup, namespace);
} else {
await this.refreshIndirectMode(namespace);
}
}
public async refreshController(showErrors: boolean = true, namespace: string): Promise<void> {
await Promise.all([
this._azApi.az.arcdata.dc.config.show(namespace, this.azAdditionalEnvVars).then(result => {
this._controllerConfig = result.stdout;
@@ -108,38 +99,6 @@ export class ControllerModel {
throw err;
})
]);
}
public async refreshDirectMode(resourceGroup: string, namespace: string): Promise<void> {
const newRegistrations: Registration[] = [];
await Promise.all([
this._azApi.az.postgres.arcserver.list(namespace, this.azAdditionalEnvVars).then(result => {
newRegistrations.push(...result.stdout.map(r => {
return {
instanceName: r.name,
state: r.state,
instanceType: ResourceType.postgresInstances
};
}));
}),
this._azApi.az.sql.miarc.list({ resourceGroup: resourceGroup, namespace: undefined }, this.azAdditionalEnvVars).then(result => {
newRegistrations.push(...result.stdout.map(r => {
return {
instanceName: r.name,
state: r.state,
instanceType: ResourceType.sqlManagedInstances
};
}));
})
]).then(() => {
this._registrations = newRegistrations;
this.registrationsLastUpdated = new Date();
this._onRegistrationsUpdated.fire(this._registrations);
});
}
public async refreshIndirectMode(namespace: string): Promise<void> {
const newRegistrations: Registration[] = [];
await Promise.all([
this._azApi.az.postgres.arcserver.list(namespace, this.azAdditionalEnvVars).then(result => {
@@ -152,14 +111,15 @@ export class ControllerModel {
}));
}),
this._azApi.az.sql.miarc.list({ resourceGroup: undefined, namespace: namespace }, this.azAdditionalEnvVars).then(result => {
newRegistrations.push(...result.stdout.map(r => {
let miaaList = parseMiaaList(result.stdout.toString());
let jsonList: azExt.SqlMiListResult[] = JSON.parse(<string>miaaList);
newRegistrations.push(...jsonList.map(r => {
return {
instanceName: r.name,
state: r.state,
instanceType: ResourceType.sqlManagedInstances
};
}));
})
]).then(() => {
this._registrations = newRegistrations;

View File

@@ -15,7 +15,6 @@ import { ConnectToMiaaSqlDialog } from '../ui/dialogs/connectMiaaDialog';
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
import { ControllerModel, Registration } from './controllerModel';
import { ResourceModel } from './resourceModel';
import { ConnectionMode } from '../constants';
export type DatabaseModel = { name: string, status: string, earliestBackup: string, lastBackup: string };
export type RPModel = { recoveryPointObjective: string, retentionDays: string };
@@ -100,25 +99,14 @@ export class MiaaModel extends ResourceModel {
try {
try {
let result;
if (this.controllerModel.info.connectionMode === ConnectionMode.direct) {
result = await this._azApi.az.sql.miarc.show(
this.info.name,
{
resourceGroup: this.controllerModel.info.resourceGroup,
namespace: undefined
},
this.controllerModel.azAdditionalEnvVars
);
} else {
result = await this._azApi.az.sql.miarc.show(
this.info.name,
{
resourceGroup: undefined,
namespace: this.controllerModel.info.namespace
},
this.controllerModel.azAdditionalEnvVars
);
}
result = await this._azApi.az.sql.miarc.show(
this.info.name,
{
resourceGroup: undefined,
namespace: this.controllerModel.info.namespace
},
this.controllerModel.azAdditionalEnvVars
);
this._config = result.stdout;
this.configLastUpdated = new Date();
this.rpSettings.retentionDays = this._config?.spec?.backup?.retentionPeriodInDays?.toString() ?? '';

View File

@@ -7,7 +7,7 @@ import { ResourceType } from 'arc';
import * as azdata from 'azdata';
import * as azurecore from 'azurecore';
import * as vscode from 'vscode';
import { getConnectionModeDisplayText, getResourceTypeIcon, resourceTypeToDisplayName } from '../../../common/utils';
import { getResourceTypeIcon, resourceTypeToDisplayName } from '../../../common/utils';
import { cssStyles, IconPathHelper, controllerTroubleshootDocsUrl, iconSize } from '../../../constants';
import * as loc from '../../../localizedConstants';
import { ControllerModel } from '../../../models/controllerModel';
@@ -147,9 +147,8 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
newInstance.onDidClick(async () => {
const node = this._controllerModel.treeDataProvider.getControllerNode(this._controllerModel);
await vscode.commands.executeCommand('azdata.resource.deploy',
'azure-sql-mi', // Default option
['azure-sql-mi', 'arc-postgres'], // Type filter
{ 'azure-sql-mi': { 'mi-type': ['arc-mi'] } }, // Options filter
'arc-sql', // Default option
['arc-sql', 'arc-postgres'], // Type filter
{ 'CONTROLLER_NAME': node?.label });
}));
@@ -218,7 +217,7 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
this.controllerProperties.resourceGroupName = config?.spec.settings.azure.resourceGroup || this.controllerProperties.resourceGroupName;
this.controllerProperties.location = this._azurecoreApi.getRegionDisplayName(config?.spec.settings.azure.location) || this.controllerProperties.location;
this.controllerProperties.subscriptionId = config?.spec.settings.azure.subscription || this.controllerProperties.subscriptionId;
this.controllerProperties.connectionMode = getConnectionModeDisplayText(config?.spec.settings.azure.connectionMode) || this.controllerProperties.connectionMode;
this.controllerProperties.connectionMode = config?.spec.settings.azure.connectionMode.toLowerCase() || this.controllerProperties.connectionMode.toLowerCase();
this.controllerProperties.instanceNamespace = config?.metadata.namespace || this.controllerProperties.instanceNamespace;
this.controllerProperties.status = config?.status.state || this.controllerProperties.status;
this.refreshDisplayedProperties();

View File

@@ -222,15 +222,15 @@ export class ControllerUpgradesPage extends DashboardPage {
try {
upgradeButton.enabled = false;
vscode.window.showInformationMessage(loc.upgradingController('kubectl get datacontrollers -A\' should not be localized.'));
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.updatingInstance(this._controllerModel.info.name),
cancellable: true
},
async (_progress, _token): Promise<void> => {
if (nextVersion !== '') {
if (this._controllerModel.info.connectionMode === ConnectionMode.direct) {
if (this._controllerModel.info.connectionMode.toLowerCase() === ConnectionMode.direct) {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.upgradingDirectDC(this._controllerModel.info.name, nextVersion, this._controllerModel.info.resourceGroup),
cancellable: true
},
async (_progress, _token): Promise<void> => {
if (nextVersion !== '') {
await this._azApi.az.arcdata.dc.upgrade(
nextVersion,
this._controllerModel.info.name,
@@ -238,24 +238,41 @@ export class ControllerUpgradesPage extends DashboardPage {
undefined, // Indirect mode argument - namespace
);
} else {
vscode.window.showInformationMessage(loc.noUpgrades);
}
try {
await this._controllerModel.refresh(false, this._controllerModel.info.namespace);
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
}
}
);
} else {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.upgradingIndirectDC(this._controllerModel.info.name, nextVersion, this._controllerModel.info.namespace),
cancellable: true
},
async (_progress, _token): Promise<void> => {
if (nextVersion !== '') {
await this._azApi.az.arcdata.dc.upgrade(
nextVersion,
this._controllerModel.info.name,
undefined, // Direct mode argument - resourceGroup
this._controllerModel.info.namespace,
);
} else {
vscode.window.showInformationMessage(loc.noUpgrades);
}
try {
await this._controllerModel.refresh(false, this._controllerModel.info.namespace);
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
}
} else {
vscode.window.showInformationMessage(loc.noUpgrades);
}
try {
await this._controllerModel.refresh(false, this._controllerModel.info.namespace);
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
}
}
);
);
}
} catch (error) {
console.log(error);
}

View File

@@ -7,7 +7,7 @@ import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as azExt from 'az-ext';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles, ConnectionMode } from '../../../constants';
import { IconPathHelper, cssStyles } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
import { MiaaModel, RPModel, DatabaseModel, systemDbs } from '../../../models/miaaModel';
import { ControllerModel } from '../../../models/controllerModel';
@@ -220,23 +220,13 @@ export class MiaaBackupsPage extends DashboardPage {
cancellable: false
},
async (_progress, _token): Promise<void> => {
if (this._miaaModel.controllerModel.info.connectionMode === ConnectionMode.direct) {
await this._azApi.az.sql.miarc.update(
this._miaaModel.info.name,
this._saveArgs,
this._miaaModel.controllerModel.info.resourceGroup,
undefined, // Indirect mode argument - namespace
undefined, // Indirect mode argument - usek8s
this._miaaModel.controllerModel.azAdditionalEnvVars);
} else {
await this._azApi.az.sql.miarc.update(
this._miaaModel.info.name,
this._saveArgs,
undefined, // Direct mode argument - resourceGroup
this._miaaModel.controllerModel.info.namespace,
true,
this._miaaModel.controllerModel.azAdditionalEnvVars);
}
await this._azApi.az.sql.miarc.update(
this._miaaModel.info.name,
this._saveArgs,
undefined, // Direct mode argument - resourceGroup
this._miaaModel.controllerModel.info.namespace,
true,
this._miaaModel.controllerModel.azAdditionalEnvVars);
try {
await this._miaaModel.refresh();
} catch (error) {

View File

@@ -7,7 +7,7 @@ import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as azExt from 'az-ext';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles, ConnectionMode } from '../../../constants';
import { IconPathHelper, cssStyles } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
import { convertToGibibyteString } from '../../../common/utils';
import { MiaaModel } from '../../../models/miaaModel';
@@ -132,23 +132,13 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
},
async (_progress, _token): Promise<void> => {
try {
if (this._miaaModel.controllerModel.info.connectionMode === ConnectionMode.direct) {
await this._azApi.az.sql.miarc.update(
this._miaaModel.info.name,
this.saveArgs,
this._miaaModel.controllerModel.info.resourceGroup,
undefined, // Indirect mode argument - namespace
undefined, // Indirect mode argument - usek8s
this._miaaModel.controllerModel.azAdditionalEnvVars);
} else {
await this._azApi.az.sql.miarc.update(
this._miaaModel.info.name,
this.saveArgs,
undefined, // Direct mode argument - resourceGroup
this._miaaModel.controllerModel.info.namespace,
true,
this._miaaModel.controllerModel.azAdditionalEnvVars);
}
await this._azApi.az.sql.miarc.update(
this._miaaModel.info.name,
this.saveArgs,
undefined, // Direct mode argument - resourceGroup
this._miaaModel.controllerModel.info.namespace,
true,
this._miaaModel.controllerModel.azAdditionalEnvVars);
} catch (err) {
this.saveButton!.enabled = true;
throw err;
@@ -272,6 +262,7 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
this.syncSecondaryToCommitBox = this.modelView.modelBuilder.inputBox().withProps({
readOnly: false,
min: -1,
max: 2,
inputType: 'number',
placeHolder: loc.loading,
ariaLabel: loc.syncSecondaryToCommit
@@ -359,8 +350,9 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
currentCPUSize = '';
}
this.coresRequestBox!.placeHolder = currentCPUSize;
this.coresRequestBox!.value = '';
this.coresRequestBox!.value = currentCPUSize;
this.coresRequestBox!.placeHolder = '';
this.saveArgs.coresRequest = undefined;
currentCPUSize = this._miaaModel.config?.spec?.scheduling?.default?.resources?.limits?.cpu;
@@ -371,6 +363,7 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
this.coresLimitBox!.placeHolder = currentCPUSize;
this.coresLimitBox!.value = '';
this.saveArgs.coresLimit = undefined;
}
@@ -400,6 +393,7 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
this.memoryLimitBox!.placeHolder = currentMemSizeConversion!;
this.memoryLimitBox!.value = '';
this.saveArgs.memoryLimit = undefined;
}

View File

@@ -8,7 +8,7 @@ import * as azExt from 'az-ext';
import * as azurecore from 'azurecore';
import * as vscode from 'vscode';
import { getDatabaseStateDisplayText, promptForInstanceDeletion } from '../../../common/utils';
import { ConnectionMode, cssStyles, IconPathHelper, miaaTroubleshootDocsUrl } from '../../../constants';
import { cssStyles, IconPathHelper, miaaTroubleshootDocsUrl } from '../../../constants';
import * as loc from '../../../localizedConstants';
import { ControllerModel } from '../../../models/controllerModel';
import { MiaaModel } from '../../../models/miaaModel';
@@ -243,25 +243,14 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
cancellable: false
},
async (_progress, _token) => {
if (this._controllerModel.info.connectionMode === ConnectionMode.direct) {
return await this._azApi.az.sql.miarc.delete(
this._miaaModel.info.name,
{
resourceGroup: this._controllerModel.info.resourceGroup,
namespace: undefined,
},
this._controllerModel.azAdditionalEnvVars
);
} else {
return await this._azApi.az.sql.miarc.delete(
this._miaaModel.info.name,
{
resourceGroup: undefined,
namespace: this._controllerModel.info.namespace,
},
this._controllerModel.azAdditionalEnvVars
);
}
return await this._azApi.az.sql.miarc.delete(
this._miaaModel.info.name,
{
resourceGroup: undefined,
namespace: this._controllerModel.info.namespace,
},
this._controllerModel.azAdditionalEnvVars
);
}
);
await this._controllerModel.refreshTreeNode();

View File

@@ -7,7 +7,7 @@ import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as azExt from 'az-ext';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles, ConnectionMode } from '../../../constants';
import { IconPathHelper, cssStyles } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
import { ControllerModel } from '../../../models/controllerModel';
import { UpgradeSqlMiaa } from '../../dialogs/upgradeSqlMiaa';
@@ -160,25 +160,14 @@ export class MiaaUpgradeManagementPage extends DashboardPage {
private async getMiaaVersion(): Promise<string | undefined> {
try {
let miaaShowResult;
if (this._controllerModel.info.connectionMode === ConnectionMode.direct || this._controllerModel.controllerConfig?.spec.settings.azure.connectionMode === ConnectionMode.direct) {
miaaShowResult = await this._azApi.az.sql.miarc.show(
this._miaaModel.info.name,
{
resourceGroup: this._controllerModel.info.resourceGroup,
namespace: undefined
},
this._controllerModel.azAdditionalEnvVars
);
} else {
miaaShowResult = await this._azApi.az.sql.miarc.show(
this._miaaModel.info.name,
{
resourceGroup: undefined,
namespace: this._controllerModel.info.namespace
},
this._controllerModel.azAdditionalEnvVars
);
}
miaaShowResult = await this._azApi.az.sql.miarc.show(
this._miaaModel.info.name,
{
resourceGroup: undefined,
namespace: this._controllerModel.info.namespace
},
this._controllerModel.azAdditionalEnvVars
);
return miaaShowResult.stdout.status.runningVersion;
} catch (e) {
console.error(loc.showMiaaError, e);
@@ -266,28 +255,17 @@ export class MiaaUpgradeManagementPage extends DashboardPage {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.updatingInstance(this._miaaModel.info.name),
title: loc.upgradingIndirectMiaa(this._miaaModel.info.name, this._controllerModel.info.namespace),
cancellable: true
},
async (_progress, _token): Promise<void> => {
if (this._controllerModel.info.connectionMode === ConnectionMode.direct) {
await this._azApi.az.sql.miarc.upgrade(
this._miaaModel.info.name,
{
resourceGroup: this._controllerModel.info.resourceGroup,
namespace: undefined
}
);
} else {
await this._azApi.az.sql.miarc.upgrade(
this._miaaModel.info.name,
{
resourceGroup: undefined,
namespace: this._controllerModel.info.namespace,
}
);
}
await this._azApi.az.sql.miarc.upgrade(
this._miaaModel.info.name,
{
resourceGroup: undefined,
namespace: this._controllerModel.info.namespace,
}
);
try {
await this._controllerModel.refresh(false, this._controllerModel.info.namespace);
} catch (error) {

View File

@@ -201,10 +201,10 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
// default info.name to the name of the controller instance if the user did not specify their own and to a pre-canned default if for some weird reason controller endpoint returned instanceName is also not a valid value
controllerModel.info.name = controllerModel.info.name || controllerModel.controllerConfig?.metadata.name || loc.defaultControllerName;
controllerModel.info.resourceGroup = <string>controllerModel.controllerConfig?.spec.settings.azure.resourceGroup;
controllerModel.info.connectionMode = <string>controllerModel.controllerConfig?.spec.settings.azure.connectionMode;
controllerModel.info.connectionMode = <string>controllerModel.controllerConfig?.spec.settings.azure.connectionMode.toLowerCase();
controllerModel.info.location = <string>controllerModel.controllerConfig?.spec.settings.azure.location;
if (controllerModel.info.connectionMode.toLowerCase() === ConnectionMode.direct.toLowerCase()) {
if (controllerModel.info.connectionMode.toLowerCase() === ConnectionMode.direct) {
const rawCustomLocation = <string>controllerModel.controllerConfig?.metadata.annotations['management.azure.com/customLocation'];
const exp = /custom[lL]ocations\/([\S]*)/;
controllerModel.info.customLocation = <string>exp.exec(rawCustomLocation)?.pop();

View File

@@ -2,7 +2,7 @@
"name": "azcli",
"displayName": "%azcli.arc.displayName%",
"description": "%azcli.arc.description%",
"version": "1.4.1",
"version": "1.5.1",
"publisher": "Microsoft",
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
"icon": "images/extension.png",

View File

@@ -5,9 +5,7 @@
import * as azExt from 'az-ext';
import { IAzTool } from './az';
import Logger from './common/logger';
import { NoAzureCLIError } from './common/utils';
import * as loc from './localizedConstants';
import { AzToolService } from './services/azToolService';
/**
@@ -21,7 +19,6 @@ export function validateAz(az: IAzTool | undefined) {
export function throwIfNoAz(localAz: IAzTool | undefined): asserts localAz {
if (!localAz) {
Logger.log(loc.noAzureCLI);
throw new NoAzureCLIError();
}
}
@@ -120,9 +117,9 @@ export function getAzApi(localAzDiscovered: Promise<IAzTool | undefined>, azTool
delete: async (
name: string,
args: {
// Direct mode arguments
// ARM API arguments
resourceGroup?: string;
// Indirect mode arguments
// K8s API arguments
namespace?: string;
},
additionalEnvVars?: azExt.AdditionalEnvVars
@@ -133,9 +130,9 @@ export function getAzApi(localAzDiscovered: Promise<IAzTool | undefined>, azTool
},
list: async (
args: {
// Direct mode arguments
// ARM API arguments
resourceGroup?: string;
// Indirect mode arguments
// K8s API arguments
namespace?: string;
},
additionalEnvVars?: azExt.AdditionalEnvVars
@@ -147,9 +144,9 @@ export function getAzApi(localAzDiscovered: Promise<IAzTool | undefined>, azTool
show: async (
name: string,
args: {
// Direct mode arguments
// ARM API arguments
resourceGroup?: string;
// Indirect mode arguments
// K8s API arguments
namespace?: string;
},
// Additional arguments
@@ -169,9 +166,9 @@ export function getAzApi(localAzDiscovered: Promise<IAzTool | undefined>, azTool
noWait?: boolean;
syncSecondaryToCommit?: string;
},
// Direct mode arguments
// ARM API arguments
resourceGroup?: string,
// Indirect mode arguments
// K8s API arguments
namespace?: string,
usek8s?: boolean,
// Additional arguments
@@ -184,9 +181,9 @@ export function getAzApi(localAzDiscovered: Promise<IAzTool | undefined>, azTool
upgrade: async (
name: string,
args: {
// Direct mode arguments
// ARM API arguments
resourceGroup?: string;
// Indirect mode arguments
// K8s API arguments
namespace?: string;
},
// Additional arguments

View File

@@ -12,7 +12,7 @@ import * as vscode from 'vscode';
import { executeCommand, executeSudoCommand, ExitCodeError, ProcessOutput } from './common/childProcess';
import { HttpClient } from './common/httpClient';
import Logger from './common/logger';
import { AzureCLIArcExtError, NoAzureCLIError, searchForCmd } from './common/utils';
import { NoAzureCLIArcExtError, NoAzureCLIError, searchForCmd } from './common/utils';
import { azArcdataInstallKey, azConfigSection, azFound, debugConfigKey, latestAzArcExtensionVersion, azCliInstallKey, azArcFound, azHostname, azUri } from './constants';
import * as loc from './localizedConstants';
@@ -116,7 +116,7 @@ export class AzTool implements azExt.IAzApi {
const argsArray = ['arcdata', 'dc', 'upgrade', '--desired-version', desiredVersion, '--name', name];
// Direct mode argument
if (resourceGroup) { argsArray.push('--resource-group', resourceGroup); }
// Indirect mode arguments
// K8s API arguments
if (namespace) {
argsArray.push('--k8s-namespace', namespace);
argsArray.push('--use-k8s');
@@ -180,9 +180,9 @@ export class AzTool implements azExt.IAzApi {
delete: (
name: string,
args: {
// Direct mode arguments
// ARM API arguments
resourceGroup?: string,
// Indirect mode arguments
// K8s API arguments
namespace?: string
// Additional arguments
},
@@ -200,14 +200,14 @@ export class AzTool implements azExt.IAzApi {
},
list: (
args: {
// Direct mode arguments
// ARM API arguments
resourceGroup?: string,
// Indirect mode arguments
// K8s API arguments
namespace?: string
// Additional arguments
},
additionalEnvVars?: azExt.AdditionalEnvVars
): Promise<azExt.AzOutput<azExt.SqlMiListResult[]>> => {
): Promise<azExt.AzOutput<azExt.SqlMiListRawOutput>> => {
const argsArray = ['sql', 'mi-arc', 'list'];
if (args.resourceGroup) {
argsArray.push('--resource-group', args.resourceGroup);
@@ -216,14 +216,15 @@ export class AzTool implements azExt.IAzApi {
argsArray.push('--k8s-namespace', args.namespace);
argsArray.push('--use-k8s');
}
return this.executeCommand<azExt.SqlMiListResult[]>(argsArray, additionalEnvVars);
return this.executeCommand<azExt.SqlMiListRawOutput>(argsArray, additionalEnvVars);
},
show: (
name: string,
args: {
// Direct mode arguments
// ARM API arguments
resourceGroup?: string,
// Indirect mode arguments
// K8s API arguments
namespace?: string
// Additional arguments
},
@@ -250,9 +251,9 @@ export class AzTool implements azExt.IAzApi {
retentionDays?: string,
syncSecondaryToCommit?: string
},
// Direct mode arguments
// ARM API arguments
resourceGroup?: string,
// Indirect mode arguments
// K8s API arguments
namespace?: string,
usek8s?: boolean,
// Additional arguments
@@ -265,6 +266,7 @@ export class AzTool implements azExt.IAzApi {
if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
if (args.noWait) { argsArray.push('--no-wait'); }
if (args.retentionDays) { argsArray.push('--retention-days', args.retentionDays); }
if (args.syncSecondaryToCommit) { argsArray.push('--sync-secondary-to-commit', args.syncSecondaryToCommit); }
if (resourceGroup) { argsArray.push('--resource-group', resourceGroup); }
if (namespace) { argsArray.push('--k8s-namespace', namespace); }
if (usek8s) { argsArray.push('--use-k8s'); }
@@ -273,9 +275,9 @@ export class AzTool implements azExt.IAzApi {
upgrade: (
name: string,
args: {
// Direct mode arguments
// ARM API arguments
resourceGroup?: string,
// Indirect mode arguments
// K8s API arguments
namespace?: string
// Additional arguments
},
@@ -447,7 +449,7 @@ export async function checkAndInstallAz(userRequested: boolean = false): Promise
try {
return await findAzAndArc(); // find currently installed Az
} catch (err) {
if (err === AzureCLIArcExtError) {
if (err instanceof NoAzureCLIArcExtError) {
// Az found but arcdata extension not found. Prompt user to install it, then check again.
if (await promptToInstallArcdata(userRequested)) {
return await findAzAndArc();
@@ -478,7 +480,7 @@ export async function findAzAndArc(): Promise<IAzTool> {
Logger.log(loc.foundExistingAz(await azTool.getPath(), (await azTool.getSemVersionAz()).raw, (await azTool.getSemVersionArc()).raw));
return azTool;
} catch (err) {
if (err === AzureCLIArcExtError) {
if (err === NoAzureCLIArcExtError) {
Logger.log(loc.couldNotFindAzArc(err));
Logger.log(loc.noAzArc);
await vscode.commands.executeCommand('setContext', azArcFound, false); // save a context key that az was not found so that command for installing az is available in commandPalette and that for updating it is no longer available.
@@ -506,7 +508,7 @@ async function findSpecificAzAndArc(): Promise<IAzTool> {
// if no az has been found. If found, check if az arcdata extension exists.
const arcVersion = parseArcExtensionVersion(versionOutput.stdout);
if (arcVersion === undefined) {
throw AzureCLIArcExtError;
throw new NoAzureCLIArcExtError;
}
// Quietly attempt to update the arcdata extension to the latest. If it is already the latest, then it will not update.

View File

@@ -17,7 +17,7 @@ export class NoAzureCLIError extends Error implements azExt.ErrorWithLink {
}
}
export class AzureCLIArcExtError extends Error implements azExt.ErrorWithLink {
export class NoAzureCLIArcExtError extends Error implements azExt.ErrorWithLink {
constructor() {
super(loc.arcdataExtensionNotInstalled);
}

View File

@@ -29,6 +29,11 @@ declare module 'az-ext' {
protocol: string // "https"
}
export interface SqlMiListRawOutput {
text: string,
miaaList: SqlMiListResult[]
}
export interface SqlMiListResult {
name: string, // "arc-miaa"
replicas: string, // "1/1"
@@ -575,9 +580,9 @@ declare module 'az-ext' {
delete(
name: string,
args: {
// Direct mode arguments
// ARM API arguments
resourceGroup?: string,
// Indirect mode arguments
// K8s API arguments
namespace?: string
},
// Additional arguments
@@ -585,20 +590,20 @@ declare module 'az-ext' {
): Promise<AzOutput<void>>,
list(
args: {
// Direct mode arguments
// ARM API arguments
resourceGroup?: string,
// Indirect mode arguments
// K8s API arguments
namespace?: string
},
// Additional arguments
additionalEnvVars?: AdditionalEnvVars
): Promise<AzOutput<SqlMiListResult[]>>,
): Promise<AzOutput<SqlMiListRawOutput>>,
show(
name: string,
args: {
// Direct mode arguments
// ARM API arguments
resourceGroup?: string,
// Indirect mode arguments
// K8s API arguments
namespace?: string
},
// Additional arguments
@@ -615,9 +620,9 @@ declare module 'az-ext' {
retentionDays?: string, //5
syncSecondaryToCommit?: string //2
},
// Direct mode arguments
// ARM API arguments
resourceGroup?: string,
// Indirect mode arguments
// K8s API arguments
namespace?: string,
usek8s?: boolean,
// Additional arguments
@@ -626,9 +631,9 @@ declare module 'az-ext' {
upgrade(
name: string,
args: {
// Direct mode arguments
// ARM API arguments
resourceGroup?: string,
// Indirect mode arguments
// K8s API arguments
namespace?: string
},
// Additional arguments

View File

@@ -42,12 +42,11 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeN
}
if (!this.initialized) {
this.loadSavedControllers().catch(err => { vscode.window.showErrorMessage(localize('bdc.controllerTreeDataProvider.error', "Unexpected error loading saved controllers: {0}", err)); });
} else {
// We set the context here since VS Code takes a bit of time to process the _onDidChangeTreeData
// and so if we set it as soon as we finished loading the controllers it would briefly flash
// the "connect to controller" welcome view
await vscode.commands.executeCommand('setContext', 'bdc.loaded', true);
try {
await this.loadSavedControllers();
} catch (err) {
void vscode.window.showErrorMessage(localize('bdc.controllerTreeDataProvider.error', "Unexpected error loading saved controllers: {0}", err));
}
}
return this.root.getChildren();
@@ -132,13 +131,12 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeN
this.root.clearChildren();
treeNodes.forEach(node => this.root.addChild(node));
this.notifyNodeChanged();
await vscode.commands.executeCommand('setContext', 'bdc.loaded', true);
} catch (err) {
// Reset so we can try again if the tree refreshes
this.initialized = false;
throw err;
}
}
public async saveControllers(): Promise<void> {

View File

@@ -68,6 +68,10 @@ export class DacFxTestService implements mssql.IDacFxService {
booleanOptionsDictionary: {
'SampleProperty1': { value: false, description: sampleDesc, displayName: sampleName },
'SampleProperty2': { value: false, description: sampleDesc, displayName: sampleName }
},
objectTypesDictionary: {
'ObjectType1': sampleName,
'ObjectType2': sampleName
}
}
};

View File

@@ -1,34 +1,36 @@
## Integration tests
The integration-tests suite is based on the extension testing feature provided by VS Code, We can use this for:
* Commands for setting up the environment for feature testing.
* Adding test cases that do not need UI interaction or the test scenarios not supported by the UI automation framework (e.g. object explorer context menu not html based)
This extension is for running tests against specific features that require a connection to an actual server.
Unit tests that don't require this should be added as tests to the extensions or core directly.
Tests that require user interaction should be added to the smoke tests - see https://github.com/microsoft/azuredatastudio/blob/main/test/smoke/README.md for more information.
##### Folders
* extensionInstallers folder: Copy the VISX installers for the extensions we would like to run the tests with.
* src folder: This is where the test file for features should be added, name the file like this: feature.test.ts. e.g. objectExplorer.test.ts
## UI automation testing
The UI automation test cases should be added under $root/test/smoke/src/sql folder. Each feature should create its own folder and add 2 files, one for accessing the feature and the other for the test cases. For example: objectExplorer.ts and objectExplorer.test.ts. only tested on Windows for now.
For both Smoke test and Integration test, ADS will be launched using new temp folders: extension folder and data folder so that your local dev environment won't be changed.
* `extensionInstallers` folder: VSIX packages of non-builtin extensions should be put here for the tests to run with, they will be installed upon startup of the tests.
* `src/test` folder: This is where the test files for features should be added, name the file like this: `feature.test.ts` e.g. `objectExplorer.test.ts`
## How to run the test
When these tests are ran, Azure Data Studio will be launched using new temp folders for installed extensions and data so that your local dev environment won't be changed.
1. In the build pipeline:
The integration test suite has been added to ADS windows pipeline to run the test and report the results, you can find the test results under the test tab.
2. Local environment:
1. Install [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli)
1. Close all currently active VS Code windows
1. open a terminal window/command line window
1. navigate to this folder and then run 'node setEnvironmentVariables.js', there are different options, by default VSCode will be opened.
1. Terminal(Mac)/CMD(Windows): node setEnvironmentVariables.js Terminal
2. Git-Bash on Windows: node setEnvironmentVariables.js BashWin
1. Follow the instructions in the window: you will be prompted to login to azure portal.
1. Open a terminal window/command line window
1. Run `az login` to login with your Microsoft AAD account.
1. Navigate to this folder and then run `node setEnvironmentVariables.js`, there are different options, by default VS Code will be opened.
1. Terminal(Mac)/CMD(Windows): `node setEnvironmentVariables.js Terminal`
2. Git-Bash on Windows: `node setEnvironmentVariables.js BashWin`
1. A new window will be opened based on your selection and the new window will have the required environment variables set.
1. Run the Test:
1. For Integration Test: in the new window navigate to the scripts folder and run sql-test-integration.bat or sql-test-integration.sh based on your environment.
2. Smoke Test can be launched in 2 ways:
1. In the new window navigate to the test/smoke folder and run: node smoke/index.js
2. Or, In a VSCode window opened by step above, open AzureDataStudio folder and then select the 'Launch Smoke Test' option.
2. In the new window navigate to the scripts folder and run sql-test-integration.[bat|sh]
## Skipping Python Installation Tests
The integration tests contain some tests that test the Python installation for Notebooks. This can take a long time to run and so if you do not need to run them you can skip them by setting the `SKIP_PYTHON_INSTALL_TEST` environment variable to `1`
## How to debug the tests
1. Set the debug target to `Attach to Extension Host`
@@ -38,6 +40,6 @@ The integration test suite has been added to ADS windows pipeline to run the tes
## Code Coverage
Code coverage for these tests is enabled by default. After running the tests you can find the results in the `coverage` folder at the root of this extension.
Code coverage is enabled by default. After running the tests you can find the results in the `coverage` folder at the root of this extension.
This code coverage covers extension code only - it will not instrument code from the core.

View File

@@ -302,7 +302,7 @@ suite('Schema compare integration test suite @DacFx@', () => {
assert(includeResult.affectedDependencies[0].included === true, 'Table t2 should be included as a result of including v1. Actual: false');
// excluding views from the comparison should make it so t2 can be excluded
deploymentOptions.excludeObjectTypes.value.push(mssql.SchemaObjectType.Views);
deploymentOptions.excludeObjectTypes.value.push(Object.keys(deploymentOptions.objectTypesDictionary).find((key) => { return deploymentOptions.objectTypesDictionary[key] === 'Views'; }));
await schemaCompareService.schemaCompare(operationId, source, target, azdata.TaskExecutionMode.execute, deploymentOptions);
const excludeResult3 = await schemaCompareService.schemaCompareIncludeExcludeNode(operationId, t2Difference, false, azdata.TaskExecutionMode.execute);
assertIncludeExcludeResult(excludeResult3, true, 0, 0);
@@ -507,7 +507,7 @@ suite('Schema compare integration test suite @DacFx@', () => {
const deploymentOptionsResult = await schemaCompareService.schemaCompareGetDefaultOptions();
let deploymentOptions = deploymentOptionsResult.defaultDeploymentOptions;
deploymentOptions.excludeObjectTypes.value.push(mssql.SchemaObjectType.TableValuedFunctions);
deploymentOptions.excludeObjectTypes.value.push(Object.keys(deploymentOptions.objectTypesDictionary).find((key) => { return deploymentOptions.objectTypesDictionary[key] === 'TableValuedFunctions'; }));
const schemaCompareResult = await schemaCompareService.schemaCompare(operationId, source, target, azdata.TaskExecutionMode.execute, deploymentOptions);
assertSchemaCompareResult(schemaCompareResult, operationId, 3);

View File

@@ -35,7 +35,7 @@
],
"markdown.markdownItPlugins": true,
"markdown.previewStyles": [
"./node_modules/katex/dist/katex.min.css",
"./notebook-out/katex.min.css",
"./preview-styles/index.css"
],
"configuration": [

View File

@@ -1,6 +1,6 @@
{
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
"version": "4.1.0.16",
"version": "4.2.1.6",
"downloadFileNames": {
"Windows_86": "win-x86-net6.0.zip",
"Windows_64": "win-x64-net6.0.zip",

View File

@@ -387,7 +387,7 @@
"mssql.parallelMessageProcessing": {
"type": "boolean",
"description": "%mssql.parallelMessageProcessing%",
"default": true
"default": false
}
}
},
@@ -511,7 +511,7 @@
},
{
"command": "mssql.newTable",
"when": "connectionProvider == MSSQL && nodeType == Folder && nodeLabel == Tables && config.workbench.enablePreviewFeatures",
"when": "connectionProvider == MSSQL && nodeType == Folder && objectType == Tables && config.workbench.enablePreviewFeatures",
"group": "0_query@1"
}
],
@@ -523,7 +523,7 @@
},
{
"command": "mssql.newTable",
"when": "connectionProvider == MSSQL && nodeType == Folder && nodeLabel == Tables && config.workbench.enablePreviewFeatures",
"when": "connectionProvider == MSSQL && nodeType == Folder && objectType == Tables && config.workbench.enablePreviewFeatures",
"group": "connection@1"
}
],

View File

@@ -163,10 +163,10 @@ declare module 'mssql' {
}
/**
* Interface containing deployment options of integer type, value property holds values from <DacFx>\Product\Source\DeploymentApi\ObjectTypes.cs enum
* Interface containing deployment options of string[] type, value property holds enum names (nothing but option name) from <DacFx>\Product\Source\DeploymentApi\ObjectTypes.cs enum
*/
export interface DacDeployOptionPropertyObject {
value: number[];
value: string[];
description: string;
displayName: string;
}
@@ -179,6 +179,8 @@ declare module 'mssql' {
excludeObjectTypes: DacDeployOptionPropertyObject;
// key will be the boolean option name
booleanOptionsDictionary: { [key: string]: DacDeployOptionPropertyBoolean };
// key will be the object type enum name (nothing but option name)
objectTypesDictionary: { [key: string]: string };
}
/*
@@ -189,81 +191,6 @@ declare module 'mssql' {
checked: boolean;
}
/**
* Values from <DacFx>\Product\Source\DeploymentApi\ObjectTypes.cs
*/
export const enum SchemaObjectType {
Aggregates = 0,
ApplicationRoles = 1,
Assemblies = 2,
AssemblyFiles = 3,
AsymmetricKeys = 4,
BrokerPriorities = 5,
Certificates = 6,
ColumnEncryptionKeys = 7,
ColumnMasterKeys = 8,
Contracts = 9,
DatabaseOptions = 10,
DatabaseRoles = 11,
DatabaseTriggers = 12,
Defaults = 13,
ExtendedProperties = 14,
ExternalDataSources = 15,
ExternalFileFormats = 16,
ExternalTables = 17,
Filegroups = 18,
Files = 19,
FileTables = 20,
FullTextCatalogs = 21,
FullTextStoplists = 22,
MessageTypes = 23,
PartitionFunctions = 24,
PartitionSchemes = 25,
Permissions = 26,
Queues = 27,
RemoteServiceBindings = 28,
RoleMembership = 29,
Rules = 30,
ScalarValuedFunctions = 31,
SearchPropertyLists = 32,
SecurityPolicies = 33,
Sequences = 34,
Services = 35,
Signatures = 36,
StoredProcedures = 37,
SymmetricKeys = 38,
Synonyms = 39,
Tables = 40,
TableValuedFunctions = 41,
UserDefinedDataTypes = 42,
UserDefinedTableTypes = 43,
ClrUserDefinedTypes = 44,
Users = 45,
Views = 46,
XmlSchemaCollections = 47,
Audits = 48,
Credentials = 49,
CryptographicProviders = 50,
DatabaseAuditSpecifications = 51,
DatabaseEncryptionKeys = 52,
DatabaseScopedCredentials = 53,
Endpoints = 54,
ErrorMessages = 55,
EventNotifications = 56,
EventSessions = 57,
LinkedServerLogins = 58,
LinkedServers = 59,
Logins = 60,
MasterKeys = 61,
Routes = 62,
ServerAuditSpecifications = 63,
ServerRoleMembership = 64,
ServerRoles = 65,
ServerTriggers = 66,
ExternalStreams = 67,
ExternalStreamingJobs = 68
}
export interface SchemaCompareObjectId {
nameParts: string[];
sqlObjectType: string;

View File

@@ -6,7 +6,7 @@ import * as path from 'path';
import * as vscode from 'vscode';
import * as constants from './../common/constants';
import { BookTreeItem } from './bookTreeItem';
import { getPinnedNotebooks, setPinnedBookPathsInConfig, IPinnedNotebook } from '../common/utils';
import { getPinnedNotebooks, setPinnedBookPathsInConfig, IPinnedNotebook, getNotebookType } from '../common/utils';
export interface IBookPinManager {
pinNotebook(notebook: BookTreeItem): Promise<boolean>;
@@ -58,7 +58,7 @@ export class BookPinManager implements IBookPinManager {
pinnedBooks.splice(existingBookIndex, 1);
modifiedPinnedBooks = true;
} else if (existingBookIndex === -1 && operation === PinBookOperation.Pin) {
let addNotebook: IPinnedNotebook = { notebookPath: bookPathToChange, bookPath: notebook.book.root, title: notebook.book.title };
let addNotebook: IPinnedNotebook = { notebookPath: bookPathToChange, bookPath: getNotebookType(notebook.book) ? notebook.book.root : undefined, title: notebook.book.title };
pinnedBooks.push(addNotebook);
modifiedPinnedBooks = true;
}

View File

@@ -16,7 +16,7 @@ import { Deferred } from '../common/promise';
import { IBookTrustManager, BookTrustManager } from './bookTrustManager';
import * as loc from '../common/localizedConstants';
import * as glob from 'fast-glob';
import { getPinnedNotebooks, confirmMessageDialog, getNotebookType, FileExtension, IPinnedNotebook, BookTreeItemType } from '../common/utils';
import { getPinnedNotebooks, getNotebookType, confirmMessageDialog, FileExtension, IPinnedNotebook, BookTreeItemType } from '../common/utils';
import { IBookPinManager, BookPinManager } from './bookPinManager';
import { BookTocManager, IBookTocManager, quickPickResults } from './bookTocManager';
import { CreateBookDialog } from '../dialog/createBookDialog';
@@ -138,7 +138,8 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
let pinStatusChanged = await this.bookPinManager.pinNotebook(bookTreeItem);
sendNotebookActionEvent(NbTelemetryView.Book, NbTelemetryAction.PinNotebook);
if (pinStatusChanged) {
bookTreeItem.contextValue = 'pinnedNotebook';
bookTreeItem.contextValue = BookTreeItemType.pinnedNotebook;
this._onDidChangeTreeData.fire(bookTreeItem);
}
}
}
@@ -148,7 +149,16 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
if (bookPathToUpdate) {
let pinStatusChanged = await this.bookPinManager.unpinNotebook(bookTreeItem);
if (pinStatusChanged) {
bookTreeItem.contextValue = getNotebookType(bookTreeItem.book);
// reset to original context value
bookTreeItem.contextValue = bookTreeItem.book.type === BookTreeItemType.Markdown ? BookTreeItemType.Markdown : getNotebookType(bookTreeItem.book);
// to search for notebook in allNotebooks dictionary we need to format uri
const notebookUri = vscode.Uri.file(bookTreeItem.book.contentPath).fsPath;
// if notebook is not in current book then it is a standalone notebook
let itemOpenedInBookTreeView = this.currentBook?.getNotebook(notebookUri) ?? this.books.find(book => book.bookPath === bookTreeItem.book.contentPath)?.getNotebook(notebookUri);
if (itemOpenedInBookTreeView) {
itemOpenedInBookTreeView.contextValue = bookTreeItem.contextValue;
this._onDidChangeTreeData.fire(itemOpenedInBookTreeView.parent);
}
}
}
}
@@ -264,7 +274,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
async addNotebookToPinnedView(bookItem: BookTreeItem): Promise<void> {
let notebookPath: string = bookItem.book.contentPath;
if (notebookPath) {
let notebookDetails: IPinnedNotebook = bookItem.book.root ? { bookPath: bookItem.book.root, notebookPath: notebookPath, title: bookItem.book.title } : { notebookPath: notebookPath };
let notebookDetails: IPinnedNotebook = getNotebookType(bookItem.book) === BookTreeItemType.savedBookNotebook ? { bookPath: bookItem.book.root, notebookPath: notebookPath, title: bookItem.book.title } : { notebookPath: notebookPath };
await this.createAndAddBookModel(notebookPath, true, notebookDetails);
}
}

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M15.4,4.9a8.3,8.3,0,0,1,0,6.2,9.009,9.009,0,0,1-1.7,2.6,9.009,9.009,0,0,1-2.6,1.7A8.112,8.112,0,0,1,8,16a7.509,7.509,0,0,1-2.6-.4,7.609,7.609,0,0,1-2.3-1.3,7.31,7.31,0,0,1-1.7-1.8L.7,11.4c-.1-.4-.3-.8-.4-1.3l1-.2a7.207,7.207,0,0,0,.9,2,8.716,8.716,0,0,0,1.6,1.7,6.9,6.9,0,0,0,1.9,1A6.184,6.184,0,0,0,8,15l1.9-.2,1.6-.8a4.9,4.9,0,0,0,1.4-1.1A4.9,4.9,0,0,0,14,11.5a7.976,7.976,0,0,0,.8-1.6A12.233,12.233,0,0,0,15,8a12.233,12.233,0,0,0-.2-1.9A7.976,7.976,0,0,0,14,4.5a4.9,4.9,0,0,0-1.1-1.4A4.9,4.9,0,0,0,11.5,2a4.61,4.61,0,0,0-1.6-.7A6.283,6.283,0,0,0,8,1a6.879,6.879,0,0,0-2,.3,5.292,5.292,0,0,0-1.7.8A4.708,4.708,0,0,0,2.8,3.4,4.6,4.6,0,0,0,1.7,5H4V6H0V2H1V4.1l.3-.4.3-.5A9.122,9.122,0,0,1,3.3,1.5,7.6,7.6,0,0,1,5.5.4,7.308,7.308,0,0,1,8,0a8.112,8.112,0,0,1,3.1.6,9.009,9.009,0,0,1,2.6,1.7A9.009,9.009,0,0,1,15.4,4.9Z" />
<polygon points="8 3 8 7.3 10.9 10.1 10.1 10.9 7 7.7 7 3 8 3" />
</svg>

Before

Width:  |  Height:  |  Size: 1002 B

View File

@@ -2,7 +2,7 @@
"name": "query-history",
"displayName": "%queryHistory.displayName%",
"description": "%queryHistory.description%",
"version": "0.2.0",
"version": "0.4.0",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
@@ -39,6 +39,19 @@
"type": "boolean",
"default": true,
"description": "%queryHistory.captureEnabledDesc%"
},
"queryHistory.doubleClickAction": {
"type": "string",
"description": "%queryHistory.doubleClickAction%",
"default": "open",
"enum": [
"open",
"run"
],
"enumDescriptions": [
"%queryHistory.doubleClickAction.open%",
"%queryHistory.doubleClickAction.run%"
]
}
}
}
@@ -170,7 +183,7 @@
{
"id": "queryHistory",
"title": "%queryHistory.displayName%",
"icon": "./images/history.png"
"icon": "$(history)"
}
]
}

View File

@@ -2,6 +2,9 @@
"queryHistory.displayName": "Query History",
"queryHistory.description": "View and run previously executed queries",
"queryHistory.captureEnabledDesc": "Whether Query History capture is enabled. If false queries executed will not be captured.",
"queryHistory.doubleClickAction": "The action taken when a history item is double clicked",
"queryHistory.doubleClickAction.open": "Open a new disconnected editor with the query from the selected history item",
"queryHistory.doubleClickAction.run": "Open a new connected editor with the query and connection from the selected history item and automatically run the query",
"queryHistory.open": "Open Query",
"queryHistory.run": "Run Query",
"queryHistory.delete": "Delete",

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const QUERY_HISTORY_CONFIG_SECTION = 'queryHistory';
export const CAPTURE_ENABLED_CONFIG_SECTION = 'captureEnabled';
export const DOUBLE_CLICK_ACTION_CONFIG_SECTION = 'doubleClickAction';
export const ITEM_SELECTED_COMMAND_ID = 'queryHistory.itemSelected';

View File

@@ -5,37 +5,89 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { QueryHistoryNode } from './queryHistoryNode';
import { DOUBLE_CLICK_ACTION_CONFIG_SECTION, ITEM_SELECTED_COMMAND_ID, QUERY_HISTORY_CONFIG_SECTION } from './constants';
import { QueryHistoryItem } from './queryHistoryItem';
import { QueryHistoryProvider } from './queryHistoryProvider';
let lastSelectedItem: { item: QueryHistoryItem | undefined, time: number | undefined } = {
item: undefined,
time: undefined
};
/**
* The time in ms between clicks to count as a double click on our tree view items
*/
const DOUBLE_CLICK_TIMEOUT_MS = 500;
export async function activate(context: vscode.ExtensionContext): Promise<void> {
const provider = new QueryHistoryProvider();
context.subscriptions.push(provider);
context.subscriptions.push(vscode.window.registerTreeDataProvider('queryHistory', provider));
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.open', async (node: QueryHistoryNode) => {
return azdata.queryeditor.openQueryDocument(
{
content: node.queryText
}, node.connectionProfile?.providerId);
const treeDataProvider = new QueryHistoryProvider();
context.subscriptions.push(treeDataProvider);
const treeView = vscode.window.createTreeView('queryHistory', {
treeDataProvider,
canSelectMany: false
});
context.subscriptions.push(treeView);
// This is an internal-only command so not adding to package.json
context.subscriptions.push(vscode.commands.registerCommand(ITEM_SELECTED_COMMAND_ID, async (selectedItem: QueryHistoryItem) => {
// VS Code doesn't provide a native way to detect a double-click so we track it ourselves by keeping track of the last item clicked and
// when it was clicked to compare, then if a click happens on the same element quickly enough we trigger the configured action
const clickTime = new Date().getTime();
if (lastSelectedItem.item === selectedItem && lastSelectedItem.time && (clickTime - lastSelectedItem.time) < DOUBLE_CLICK_TIMEOUT_MS) {
const doubleClickAction = vscode.workspace.getConfiguration(QUERY_HISTORY_CONFIG_SECTION).get<string>(DOUBLE_CLICK_ACTION_CONFIG_SECTION);
switch (doubleClickAction) {
case 'run':
await runQuery(selectedItem);
break;
case 'open':
default:
await openQuery(selectedItem);
break;
}
// Clear out the last selected item so we don't run the command again on a 3rd click
lastSelectedItem = {
item: undefined,
time: undefined
};
} else {
// Update the last selected item since we didn't run a command
lastSelectedItem = {
item: selectedItem,
time: clickTime
};
}
}));
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.run', async (node: QueryHistoryNode) => {
const doc = await azdata.queryeditor.openQueryDocument(
{
content: node.queryText
}, node.connectionProfile?.providerId);
await azdata.queryeditor.connect(doc.uri, node.connectionProfile?.connectionId || '');
azdata.queryeditor.runQuery(doc.uri);
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.open', async (item: QueryHistoryItem) => {
return openQuery(item);
}));
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.delete', (node: QueryHistoryNode) => {
provider.deleteNode(node);
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.run', async (item: QueryHistoryItem) => {
return runQuery(item);
}));
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.delete', (item: QueryHistoryItem) => {
treeDataProvider.deleteItem(item);
}));
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.clear', () => {
provider.clearAll();
treeDataProvider.clearAll();
}));
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.disableCapture', async () => {
return provider.setCaptureEnabled(false);
return treeDataProvider.setCaptureEnabled(false);
}));
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.enableCapture', async () => {
return provider.setCaptureEnabled(true);
return treeDataProvider.setCaptureEnabled(true);
}));
}
async function openQuery(item: QueryHistoryItem): Promise<void> {
await azdata.queryeditor.openQueryDocument(
{
content: item.queryText
}, item.connectionProfile?.providerId);
}
async function runQuery(item: QueryHistoryItem): Promise<void> {
const doc = await azdata.queryeditor.openQueryDocument(
{
content: item.queryText
}, item.connectionProfile?.providerId);
await azdata.queryeditor.connect(doc.uri, item.connectionProfile?.connectionId || '');
azdata.queryeditor.runQuery(doc.uri);
}

View File

@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
export interface QueryHistoryItem {
readonly queryText: string,
readonly connectionProfile: azdata.connection.ConnectionProfile | undefined,
readonly timestamp: Date,
readonly isSuccess: boolean
}

View File

@@ -1,22 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { removeNewLines } from './utils';
export class QueryHistoryNode extends vscode.TreeItem {
constructor(
public readonly queryText: string,
public readonly connectionProfile: azdata.connection.ConnectionProfile | undefined,
timestamp: Date,
isSuccess: boolean
) {
super(removeNewLines(queryText), vscode.TreeItemCollapsibleState.None);
this.iconPath = isSuccess ? new vscode.ThemeIcon('check', new vscode.ThemeColor('testing.iconPassed')) : new vscode.ThemeIcon('error', new vscode.ThemeColor('testing.iconFailed'));
this.tooltip = queryText;
this.description = connectionProfile ? `${connectionProfile.serverName}|${connectionProfile.databaseName} ${timestamp.toLocaleString()}` : timestamp.toLocaleString();
}
}

View File

@@ -6,18 +6,20 @@
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import { EOL } from 'os';
import { QueryHistoryNode } from './queryHistoryNode';
import { QueryHistoryItem } from './queryHistoryItem';
import { removeNewLines } from './utils';
import { CAPTURE_ENABLED_CONFIG_SECTION, ITEM_SELECTED_COMMAND_ID, QUERY_HISTORY_CONFIG_SECTION } from './constants';
const QUERY_HISTORY_CONFIG_SECTION = 'queryHistory';
const CAPTURE_ENABLED_CONFIG_SECTION = 'captureEnabled';
const DEFAULT_CAPTURE_ENABLED = true;
const successIcon = new vscode.ThemeIcon('check', new vscode.ThemeColor('testing.iconPassed'));
const failedIcon = new vscode.ThemeIcon('error', new vscode.ThemeColor('testing.iconFailed'));
export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistoryNode>, vscode.Disposable {
export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistoryItem>, vscode.Disposable {
private _onDidChangeTreeData: vscode.EventEmitter<QueryHistoryNode | undefined> = new vscode.EventEmitter<QueryHistoryNode | undefined>();
readonly onDidChangeTreeData: vscode.Event<QueryHistoryNode | undefined> = this._onDidChangeTreeData.event;
private _onDidChangeTreeData: vscode.EventEmitter<QueryHistoryItem | undefined> = new vscode.EventEmitter<QueryHistoryItem | undefined>();
readonly onDidChangeTreeData: vscode.Event<QueryHistoryItem | undefined> = this._onDidChangeTreeData.event;
private _queryHistoryNodes: QueryHistoryNode[] = [];
private _queryHistoryItems: QueryHistoryItem[] = [];
private _captureEnabled: boolean = true;
private _disposables: vscode.Disposable[] = [];
@@ -37,10 +39,10 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
}
// Combine all the text from the batches back together
const queryText = queryInfo.batchRanges.map(r => textDocument.getText(r) ?? '').join(EOL);
const connProfile = await azdata.connection.getConnection(document.uri);
const isError = queryInfo.messages.find(m => m.isError) ? false : true;
const connectionProfile = await azdata.connection.getConnection(document.uri);
const isSuccess = queryInfo.messages.find(m => m.isError) ? false : true;
// Add to the front of the list so the new item appears at the top
this._queryHistoryNodes.unshift(new QueryHistoryNode(queryText, connProfile, new Date(), isError));
this._queryHistoryItems.unshift({ queryText, connectionProfile, timestamp: new Date(), isSuccess });
this._onDidChangeTreeData.fire(undefined);
}
}
@@ -54,21 +56,26 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
}
public clearAll(): void {
this._queryHistoryNodes = [];
this._queryHistoryItems = [];
this._onDidChangeTreeData.fire(undefined);
}
public deleteNode(node: QueryHistoryNode): void {
this._queryHistoryNodes = this._queryHistoryNodes.filter(n => n !== node);
public deleteItem(item: QueryHistoryItem): void {
this._queryHistoryItems = this._queryHistoryItems.filter(n => n !== item);
this._onDidChangeTreeData.fire(undefined);
}
public getTreeItem(node: QueryHistoryNode): vscode.TreeItem {
return node;
public getTreeItem(item: QueryHistoryItem): vscode.TreeItem {
const treeItem = new vscode.TreeItem(removeNewLines(item.queryText), vscode.TreeItemCollapsibleState.None);
treeItem.iconPath = item.isSuccess ? successIcon : failedIcon;
treeItem.tooltip = item.queryText;
treeItem.description = item.connectionProfile ? `${item.connectionProfile.serverName}|${item.connectionProfile.databaseName} ${item.timestamp.toLocaleString()}` : item.timestamp.toLocaleString();
treeItem.command = { title: '', command: ITEM_SELECTED_COMMAND_ID, arguments: [item] };
return treeItem;
}
public getChildren(element?: QueryHistoryNode): QueryHistoryNode[] {
public getChildren(element?: QueryHistoryItem): QueryHistoryItem[] {
// We only have top level items
return this._queryHistoryNodes;
return this._queryHistoryItems;
}
public dispose(): void {

View File

@@ -10,7 +10,7 @@ import 'mocha';
import * as sinon from 'sinon';
import * as azdataTest from '@microsoft/azdata-test';
import { QueryHistoryProvider } from '../queryHistoryProvider';
import { QueryHistoryNode } from '../queryHistoryNode';
import { QueryHistoryItem } from '../queryHistoryItem';
import { EOL } from 'os';
describe('QueryHistoryProvider', () => {
@@ -77,7 +77,7 @@ describe('QueryHistoryProvider', () => {
});
const children = testProvider.getChildren();
should(children).length(1, 'Should have one child after adding item');
should(children[0].queryText).be.equal(`${rangeWithContent1.content}${EOL}${rangeWithContent2.content}`, 'node content should be combined from both source ranges');
should(children[0].queryText).be.equal(`${rangeWithContent1.content}${EOL}${rangeWithContent2.content}`, 'item content should be combined from both source ranges');
});
it('event with errors is marked as error', async function () {
@@ -87,7 +87,7 @@ describe('QueryHistoryProvider', () => {
await fireQueryEventAndWaitForRefresh('queryStop', <any>{ uri: testUri.toString() }, { messages: [ message1, message2, message3 ], batchRanges: []});
const children = testProvider.getChildren();
should(children).length(1, 'Should have one child after adding item');
should((<vscode.ThemeIcon>children[0].iconPath).id).be.equal('error', 'Event with errors should have error icon');
should(children[0].isSuccess).be.false('Event with errors should have error icon');
});
it('event without errors is marked as success', async function () {
@@ -97,12 +97,12 @@ describe('QueryHistoryProvider', () => {
await fireQueryEventAndWaitForRefresh('queryStop', <any>{ uri: testUri.toString() }, { messages: [ message1, message2, message3 ], batchRanges: []});
const children = testProvider.getChildren();
should(children).length(1, 'Should have one child after adding item');
should((<vscode.ThemeIcon>children[0].iconPath).id).be.equal('check', 'Event without errors should have check icon');
should(children[0].isSuccess).be.true('Event without errors should have check icon');
});
it('queryStop events from unknown document are ignored', async function () {
const unknownUri = vscode.Uri.parse('untitled://query2');
// Since we didn't find the text document we'll never update the node list so add a timeout since that event will never fire
// Since we didn't find the text document we'll never update the item list so add a timeout since that event will never fire
await fireQueryEventAndWaitForRefresh('queryStop', <any>{ uri: unknownUri.toString() }, { messages: [], batchRanges: [] }, 2000);
const children = testProvider.getChildren();
should(children).length(0, 'Should not have any children');
@@ -113,7 +113,7 @@ describe('QueryHistoryProvider', () => {
let children = testProvider.getChildren();
should(children).length(1, 'Should have one child after adding item');
await waitForNodeRefresh(() => testProvider.clearAll());
await waitForItemRefresh(() => testProvider.clearAll());
children = testProvider.getChildren();
should(children).length(0, 'Should have no children after clearing');
});
@@ -125,53 +125,53 @@ describe('QueryHistoryProvider', () => {
let children = testProvider.getChildren();
should(children).length(3, 'Should have 3 children after adding item');
await waitForNodeRefresh(() => testProvider.clearAll());
await waitForItemRefresh(() => testProvider.clearAll());
children = testProvider.getChildren();
should(children).length(0, 'Should have no children after clearing');
});
it('delete node when no nodes doesn\'t throw', async function () {
const testNode: QueryHistoryNode = { queryText: 'SELECT 1', connectionProfile: azdataTest.stubs.connectionProfile.createConnectionProfile() };
await waitForNodeRefresh(() => testProvider.deleteNode(testNode));
it('delete item when no items doesn\'t throw', async function () {
const testItem: QueryHistoryItem = { queryText: 'SELECT 1', connectionProfile: azdataTest.stubs.connectionProfile.createConnectionProfile(), timestamp: new Date(), isSuccess: true };
await waitForItemRefresh(() => testProvider.deleteItem(testItem));
const children = testProvider.getChildren();
should(children).length(0, 'Should have no children after deleting node');
should(children).length(0, 'Should have no children after deleting item');
});
it('delete node that doesn\'t exist doesn\'t throw', async function () {
it('delete item that doesn\'t exist doesn\'t throw', async function () {
await fireQueryEventAndWaitForRefresh('queryStop', <any>{ uri: testUri.toString() }, { messages: [], batchRanges: [] });
let children = testProvider.getChildren();
should(children).length(1, 'Should have 1 child initially');
const testNode: QueryHistoryNode = { queryText: 'SELECT 1', connectionProfile: azdataTest.stubs.connectionProfile.createConnectionProfile() };
await waitForNodeRefresh(() => testProvider.deleteNode(testNode));
const testItem: QueryHistoryItem = { queryText: 'SELECT 1', connectionProfile: azdataTest.stubs.connectionProfile.createConnectionProfile(), timestamp: new Date(), isSuccess: true };
await waitForItemRefresh(() => testProvider.deleteItem(testItem));
children = testProvider.getChildren();
should(children).length(1, 'Should still have 1 child after deleting node');
should(children).length(1, 'Should still have 1 child after deleting item');
});
it('can delete single node', async function () {
it('can delete single item', async function () {
await fireQueryEventAndWaitForRefresh('queryStop', <any>{ uri: testUri.toString() }, { messages: [], batchRanges: [] });
await fireQueryEventAndWaitForRefresh('queryStop', <any>{ uri: testUri.toString() }, { messages: [], batchRanges: [] });
await fireQueryEventAndWaitForRefresh('queryStop', <any>{ uri: testUri.toString() }, { messages: [], batchRanges: [] });
const firstChildren = testProvider.getChildren();
should(firstChildren).length(3, 'Should have 3 children initially');
let nodeToDelete: QueryHistoryNode = firstChildren[1];
await waitForNodeRefresh(() => testProvider.deleteNode(nodeToDelete));
let itemToDelete: QueryHistoryItem = firstChildren[1];
await waitForItemRefresh(() => testProvider.deleteItem(itemToDelete));
const secondChildren = testProvider.getChildren();
should(secondChildren).length(2, 'Should still have 2 child after deleting node');
should(secondChildren[0]).be.equal(firstChildren[0], 'First node should still exist after deleting first node');
should(secondChildren[1]).be.equal(firstChildren[2], 'Second node should still exist after deleting first node');
should(secondChildren).length(2, 'Should still have 2 child after deleting item');
should(secondChildren[0]).be.equal(firstChildren[0], 'First item should still exist after deleting first item');
should(secondChildren[1]).be.equal(firstChildren[2], 'Second item should still exist after deleting first item');
nodeToDelete = secondChildren[0];
await waitForNodeRefresh(() => testProvider.deleteNode(nodeToDelete));
itemToDelete = secondChildren[0];
await waitForItemRefresh(() => testProvider.deleteItem(itemToDelete));
const thirdChildren = testProvider.getChildren();
should(thirdChildren).length(1, 'Should still have 1 child after deleting node');
should(thirdChildren[0]).be.equal(secondChildren[1], 'Second node should still exist after deleting second node');
should(thirdChildren).length(1, 'Should still have 1 child after deleting item');
should(thirdChildren[0]).be.equal(secondChildren[1], 'Second item should still exist after deleting second item');
nodeToDelete = thirdChildren[0];
await waitForNodeRefresh(() => testProvider.deleteNode(nodeToDelete));
itemToDelete = thirdChildren[0];
await waitForItemRefresh(() => testProvider.deleteItem(itemToDelete));
const fourthChildren = testProvider.getChildren();
should(fourthChildren).length(0, 'Should have no children after deleting all nodes');
should(fourthChildren).length(0, 'Should have no children after deleting all items');
});
it('pausing capture causes children not to be added', async function () {
@@ -192,10 +192,10 @@ describe('QueryHistoryProvider', () => {
});
async function fireQueryEventAndWaitForRefresh(type: azdata.queryeditor.QueryEventType, document: azdata.queryeditor.QueryDocument, queryInfo: azdata.queryeditor.QueryInfo, timeoutMs?: number): Promise<void> {
await waitForNodeRefresh(() => testListener.onQueryEvent(type, document, undefined, queryInfo), timeoutMs);
await waitForItemRefresh(() => testListener.onQueryEvent(type, document, undefined, queryInfo), timeoutMs);
}
async function waitForNodeRefresh(func: Function, timeoutMs?: number): Promise<void> {
async function waitForItemRefresh(func: Function, timeoutMs?: number): Promise<void> {
const promises: Promise<any>[] = [azdataTest.helpers.eventToPromise(testProvider.onDidChangeTreeData)];
const timeoutPromise = timeoutMs ? new Promise<void>(r => setTimeout(() => r(), timeoutMs)) : undefined;
if (timeoutPromise) {

View File

@@ -202,7 +202,7 @@ This defines the set of options for this field to display. There are a number of
* String array (`string[]`) - A static list of values that will be shown as a dropdown. Default value selected is defined as `FieldInfo.defaultValue`.
* CategoryValue array (`azdata.CategoryValue[]`) - A static list of CategoryValue objects that will be shown as a dropdown. Each value will define a display name separate from its value - use this for values you want to display differently to the user (such as names for an Azure region).
* CategoryValue array (`azdata.CategoryValue[]`) - A static list of CategoryValue objects that will be shown as a dropdown. Each value will define a display name separate from its value - use this for values you want to display differently to the user (such as names for an Azure region). If you use a CategoryValue array as your options, ensure you set the defaultValue to the CategoryValue's displayName rather than the name.
* [OptionsInfo](#optionsinfo) - An object allowing more control over the option values.

View File

@@ -0,0 +1,275 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"azdata_cell_guid": "e5fb2be9-e904-4821-8473-b69b90760c6a"
},
"source": [
"![Microsoft](https://raw.githubusercontent.com/microsoft/azuredatastudio/main/extensions/resource-deployment/images/microsoft-small-logo.png)\n",
"## Run SQL Server 2022 Preview container image with Docker\n",
"This notebook will use Docker to pull and run the SQL Server 2022 Preview container image and connect to it in Azure Data Studio\n",
"\n",
"### Dependencies\n",
"- Docker Engine. For more information, see [Install Docker](https://docs.docker.com/engine/installation/).\n",
"\n",
"<span style=\"color:red\"><font size=\"3\">Please press the \"Run all\" button to run the notebook</font></span>"
]
},
{
"cell_type": "markdown",
"metadata": {
"azdata_cell_guid": "76c571ab-358a-4b07-810c-53020ee1745a"
},
"source": [
"### Check dependencies"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"azdata_cell_guid": "6196300e-f896-489b-8dca-b2c42eda2d6d",
"tags": [
"hide_input"
]
},
"outputs": [],
"source": [
"import sys,os,getpass,json,html,time\n",
"from string import Template\n",
"\n",
"def run_command(displayCommand = \"\"):\n",
" print(\"Executing: \" + displayCommand if displayCommand != \"\" else cmd)\n",
" !{cmd}\n",
" if _exit_code != 0:\n",
" sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n')\n",
" print(f'Command successfully executed')\n",
"\n",
"cmd = 'docker version'\n",
"run_command()"
]
},
{
"cell_type": "markdown",
"metadata": {
"azdata_cell_guid": "87b07614-d57d-4731-ac3e-a8b324d231f2"
},
"source": [
"### List existing containers\n",
"You can view the ports that have been used by existing containers"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"azdata_cell_guid": "26170d1b-4332-4383-bcc4-1d97030daffc",
"tags": [
"hide_input"
]
},
"outputs": [],
"source": [
"cmd = f'docker ps -a'\n",
"run_command()"
]
},
{
"cell_type": "markdown",
"metadata": {
"azdata_cell_guid": "52b1faf2-d7c7-446b-ba0b-4f8b744da0bb"
},
"source": [
"### Required information"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"azdata_cell_guid": "93cb0147-7bf6-4630-b796-3811dfd1354b",
"tags": [
"hide_input"
]
},
"outputs": [],
"source": [
"env_var_flag = \"AZDATA_NB_VAR_DOCKER_PASSWORD\" in os.environ\n",
"password_name = 'SQL Server sa account password'\n",
"if env_var_flag:\n",
" sql_password = os.environ[\"AZDATA_NB_VAR_DOCKER_PASSWORD\"]\n",
" sql_port = os.environ[\"AZDATA_NB_VAR_DOCKER_PORT\"]\n",
"else:\n",
" sql_password = getpass.getpass(prompt = password_name)\n",
" password_confirm = getpass.getpass(prompt = f'Confirm {password_name}')\n",
" if sql_password != password_confirm:\n",
" raise SystemExit(f'{password_name} does not match the confirmation password.')\n",
" sql_port = input('SQL Server port, default value is 1433')\n",
" if len(sql_port) == 0:\n",
" sql_port = '1433'\n",
"print(f'{password_name}: ******')\n",
"print(f'Port: {sql_port}')"
]
},
{
"cell_type": "markdown",
"metadata": {
"azdata_cell_guid": "643ccaca-fd1d-4482-b81e-aee29b627e34"
},
"source": [
"### Pull the container image"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"azdata_cell_guid": "7b102447-3198-488f-a995-982ae1fc8555",
"tags": [
"hide_input"
]
},
"outputs": [],
"source": [
"cmd = f'docker pull mcr.microsoft.com/mssql/server:2022-latest'\n",
"run_command()"
]
},
{
"cell_type": "markdown",
"metadata": {
"azdata_cell_guid": "a4527a5f-c2c5-4f60-bfd1-b119576178c5"
},
"source": [
"### Start a new container"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"azdata_cell_guid": "82f27460-88eb-4484-92ee-40305e650d70",
"tags": [
"hide_input"
]
},
"outputs": [],
"source": [
"if env_var_flag:\n",
" container_name = os.environ[\"AZDATA_NB_VAR_DOCKER_CONTAINER_NAME\"]\n",
"else:\n",
" container_name = 'sql2022-' + time.strftime(\"%Y%m%d%H%M%S\", time.localtime())\n",
"print('New container name: ' + container_name)\n",
"\n",
"template = Template(f'docker run -e ACCEPT_EULA=Y -e \"SA_PASSWORD=$password\" -p {sql_port}:1433 --name {container_name} -d mcr.microsoft.com/mssql/server:2022-latest')\n",
"cmd = template.substitute(password=sql_password)\n",
"run_command(template.substitute(password='******'))"
]
},
{
"cell_type": "markdown",
"metadata": {
"azdata_cell_guid": "e267aa7d-dd22-43ac-9b03-cf282ef15f67"
},
"source": [
"### List all the containers"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"azdata_cell_guid": "211ee198-f1d1-4781-9daa-8497c2665de6",
"tags": [
"hide_input"
]
},
"outputs": [],
"source": [
"cmd = f'docker ps -a'\n",
"run_command()"
]
},
{
"cell_type": "markdown",
"metadata": {
"azdata_cell_guid": "5f5860c4-7962-439e-a15b-7f24f504dc18"
},
"source": [
"### Connect to SQL Server in Azure Data Studio\n",
"It might take a couple minutes for SQL Server to launch"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"azdata_cell_guid": "4bc64915-c5ae-4507-8fb0-9e413ccc2fd0",
"tags": [
"hide_input"
]
},
"outputs": [],
"source": [
"from IPython.display import *\n",
"connectionParameter = '{\"serverName\":\"localhost,' + sql_port + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":\"sa\",\"password\":' + json.dumps(sql_password) + '}'\n",
"display(HTML('<br/><a href=\"command:azdata.connect?' + html.escape(connectionParameter)+'\"><font size=\"3\">Click here to connect to SQL Server</font></a><br/>'))\n",
"display(HTML('<br/><span style=\"color:red\"><font size=\"2\">NOTE: The SQL Server password is included in this link, you may want to clear the results of this code cell before saving the notebook.</font></span>'))"
]
},
{
"cell_type": "markdown",
"metadata": {
"azdata_cell_guid": "9a1039fa-fdd3-408b-b649-8fde0fcee660"
},
"source": [
"### Stop and remove the container"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"azdata_cell_guid": "f9e0f1ad-ba6e-4c17-84ea-cc5dceb1289b",
"tags": [
"hide_input"
]
},
"outputs": [],
"source": [
"stop_container_command = f'docker stop {container_name}'\n",
"remove_container_command = f'docker rm {container_name}'\n",
"display(HTML(\"Use this link to: <a href=\\\"command:workbench.action.terminal.focus\\\">open the terminal window in Azure Data Studio</a> and use the links below to paste the command to the terminal.\"))\n",
"display(HTML(\"Stop the container: <a href=\\\"command:workbench.action.terminal.sendSequence?%7B%22text%22%3A%22\"+stop_container_command.replace(\" \",\"%20\")+\"%22%7D\\\">\" + stop_container_command + \"</a>\"))\n",
"display(HTML(\"Remove the container: <a href=\\\"command:workbench.action.terminal.sendSequence?%7B%22text%22%3A%22\"+remove_container_command.replace(\" \",\"%20\")+\"%22%7D\\\">\" + remove_container_command + \"</a>\"))"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3.10.1 64-bit",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.1"
},
"vscode": {
"interpreter": {
"hash": "878db934b706db2770cee331c11f15a67312cefb4f2334de757c7c9b6e34ef9f"
}
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -21,12 +21,12 @@
"type": "git",
"url": "https://github.com/Microsoft/azuredatastudio.git"
},
"capabilities": {
"capabilities": {
"virtualWorkspaces": false,
"untrustedWorkspaces": {
"supported": true
}
},
},
"extensionDependencies": [
"microsoft.mssql",
"microsoft.notebook"
@@ -86,6 +86,10 @@
"name": "version",
"displayName": "%version-display-name%",
"values": [
{
"name": "sql2022",
"displayName": "%sql-2022-display-name%"
},
{
"name": "sql2019",
"displayName": "%sql-2019-display-name%"
@@ -201,6 +205,58 @@
}
],
"when": "version=sql2019"
},
{
"name": "sql-image_2022",
"dialog": {
"notebook": "./notebooks/docker/2022/deploy-sql2022-image.ipynb",
"title": "%docker-sql-2022-title%",
"name": "docker-sql-2022-dialog",
"tabs": [
{
"title": "",
"sections": [
{
"title": "",
"fields": [
{
"label": "%docker-container-name-field%",
"variableName": "AZDATA_NB_VAR_DOCKER_CONTAINER_NAME",
"type": "datetime_text",
"defaultValue": "SQL2022-",
"required": true
},
{
"label": "%docker-sql-password-field%",
"variableName": "AZDATA_NB_VAR_DOCKER_PASSWORD",
"type": "sql_password",
"userName": "sa",
"confirmationRequired": true,
"confirmationLabel": "%docker-confirm-sql-password-field%",
"defaultValue": "",
"required": true
},
{
"label": "%docker-sql-port-field%",
"variableName": "AZDATA_NB_VAR_DOCKER_PORT",
"type": "number",
"defaultValue": "1433",
"required": true,
"min": 1,
"max": 65535
}
]
}
]
}
]
},
"requiredTools": [
{
"name": "docker"
}
],
"when": "version=sql2022"
}
]
},
@@ -225,6 +281,10 @@
"name": "version",
"displayName": "%version-display-name%",
"values": [
{
"name": "sql2022",
"displayName": "%sql-2022-display-name%"
},
{
"name": "sql2019",
"displayName": "%sql-2019-display-name%"
@@ -248,6 +308,12 @@
"downloadUrl": "https://go.microsoft.com/fwlink/?linkid=866662",
"requiredTools": [],
"when": "version=sql2019"
},
{
"name": "sql-windows-setup_2022",
"webPageUrl": "https://go.microsoft.com/fwlink/?linkid=2195585",
"requiredTools": [],
"when": "version=sql2022"
}
]
},

View File

@@ -8,8 +8,10 @@
"version-display-name": "Version",
"sql-2017-display-name": "SQL Server 2017",
"sql-2019-display-name": "SQL Server 2019",
"sql-2022-display-name": "SQL Server 2022 Preview",
"docker-sql-2017-title": "Deploy SQL Server 2017 container images",
"docker-sql-2019-title": "Deploy SQL Server 2019 container images",
"docker-sql-2022-title": "Deploy SQL Server 2022 Preview container images",
"docker-container-name-field": "Container name",
"docker-sql-password-field": "SQL Server password",
"docker-confirm-sql-password-field": "Confirm password",

View File

@@ -244,9 +244,9 @@ export class ServiceSettingsPage extends ResourceTypePage {
description: localize('deployCluster.AdvancedStorageDescription', "By default Controller storage settings will be applied to other services as well, you can expand the advanced storage settings to configure storage for other services.")
});
const controllerDataStorageClassInputInfo = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.controllerDataStorageClass', "Controller's data storage class"), width: inputWidth, required: true });
const controllerDataStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.controllerDataStorageClaimSize', "Controller's data storage claim size"), width: inputWidth, required: true, min: 1 });
const controllerDataStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.controllerDataStorageClaimSize', "Controller's data storage claim size (Gigabytes)"), width: inputWidth, required: true, min: 1 });
const controllerLogsStorageClassInputInfo = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.controllerLogsStorageClass', "Controller's logs storage class"), width: inputWidth, required: true });
const controllerLogsStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.controllerLogsStorageClaimSize', "Controller's logs storage claim size"), width: inputWidth, required: true, min: 1 });
const controllerLogsStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.controllerLogsStorageClaimSize', "Controller's logs storage claim size (Gigabytes)"), width: inputWidth, required: true, min: 1 });
const storagePoolLabel = createLabel(view,
{
@@ -256,9 +256,9 @@ export class ServiceSettingsPage extends ResourceTypePage {
});
const storagePoolDataStorageClassInputInfo = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.storagePoolDataStorageClass', "Storage pool's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
const storagePoolDataStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.storagePoolDataStorageClaimSize', "Storage pool's data storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
const storagePoolDataStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.storagePoolDataStorageClaimSize', "Storage pool's data storage claim size (Gigabytes)"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
const storagePoolLogsStorageClassInputInfo = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.storagePoolLogsStorageClass', "Storage pool's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
const storagePoolLogsStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.storagePoolLogsStorageClaimSize', "Storage pool's logs storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
const storagePoolLogsStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.storagePoolLogsStorageClaimSize', "Storage pool's logs storage claim size (Gigabytes)"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
const dataPoolLabel = createLabel(view,
{
@@ -267,9 +267,9 @@ export class ServiceSettingsPage extends ResourceTypePage {
required: false
});
const dataPoolDataStorageClassInputInfo = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.dataPoolDataStorageClass', "Data pool's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
const dataPoolDataStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.dataPoolDataStorageClaimSize', "Data pool's data storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
const dataPoolDataStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.dataPoolDataStorageClaimSize', "Data pool's data storage claim size (Gigabytes)"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
const dataPoolLogsStorageClassInputInfo = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.dataPoolLogsStorageClass', "Data pool's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
const dataPoolLogsStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.dataPoolLogsStorageClaimSize', "Data pool's logs storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
const dataPoolLogsStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.dataPoolLogsStorageClaimSize', "Data pool's logs storage claim size (Gigabytes)"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
const sqlServerMasterLabel = createLabel(view,
@@ -280,9 +280,9 @@ export class ServiceSettingsPage extends ResourceTypePage {
});
const sqlServerMasterDataStorageClassInputInfo = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.sqlServerMasterDataStorageClass', "SQL Server master's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
const sqlServerMasterDataStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.sqlServerMasterDataStorageClaimSize', "SQL Server master's data storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
const sqlServerMasterDataStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.sqlServerMasterDataStorageClaimSize', "SQL Server master's data storage claim size (Gigabytes)"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
const sqlServerMasterLogsStorageClassInputInfo = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.sqlServerMasterLogsStorageClass', "SQL Server master's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
const sqlServerMasterLogsStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.sqlServerMasterLogsStorageClaimSize', "SQL Server master's logs storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
const sqlServerMasterLogsStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.sqlServerMasterLogsStorageClaimSize', "SQL Server master's logs storage claim size (Gigabytes)"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
this.onNewInputComponentCreated(VariableNames.ControllerDataStorageClassName_VariableName, controllerDataStorageClassInputInfo);
this.onNewInputComponentCreated(VariableNames.ControllerDataStorageSize_VariableName, controllerDataStorageClaimSizeInputInfo);

View File

@@ -2,7 +2,7 @@
"name": "schema-compare",
"displayName": "%displayName%",
"description": "%description%",
"version": "1.15.0",
"version": "1.16.0",
"publisher": "Microsoft",
"preview": false,
"engines": {

View File

@@ -63,7 +63,7 @@ export class SchemaCompareOptionsDialog {
protected execute(): void {
// Update the model deploymentoptions with the updated table component values
this.optionsModel.setDeploymentOptions();
this.optionsModel.setObjectTypeOptions();
this.optionsModel.setIncludeObjectTypesToDeploymentOptions();
// Set the publish deploymentoptions with the updated table component values
this.schemaComparison.setDeploymentOptions(this.optionsModel.deploymentOptions);
@@ -102,6 +102,7 @@ export class SchemaCompareOptionsDialog {
// reset optionsvalueNameLookup with fresh deployment options
this.optionsModel.setOptionsToValueNameLookup();
this.optionsModel.setIncludeObjectTypesLookup();
await this.updateOptionsTable();
this.optionsFlexBuilder.removeItem(this.optionsTable);
@@ -182,11 +183,13 @@ export class SchemaCompareOptionsDialog {
this.objectsTable = view.modelBuilder.table().component();
await this.updateObjectsTable();
// Update inlcude object type options value on checkbox onchange
this.disposableListeners.push(this.objectsTable.onCellAction((rowState) => {
let checkboxState = <azdata.ICheckboxCellActionEventArgs>rowState;
if (checkboxState && checkboxState.row !== undefined) {
let label = this.optionsModel.objectTypeLabels[checkboxState.row];
this.optionsModel.objectsLookup[label] = checkboxState.checked;
// data[row][1] contains the include object type option display name
const displayName = this.objectsTable?.data[checkboxState.row][1];
this.optionsModel.setIncludeObjectTypesOptionValue(displayName, checkboxState.checked);
this.optionsChanged = true;
}
}));
@@ -229,7 +232,7 @@ export class SchemaCompareOptionsDialog {
}
private async updateObjectsTable(): Promise<void> {
let data = this.optionsModel.getObjectsData();
let data = this.optionsModel.getIncludeObjectTypesOptionsData();
await this.objectsTable.updateProperties({
data: data,
columns: [

View File

@@ -91,77 +91,6 @@ export const save: string = localize('schemaCompare.saveFile', "Save");
export function getConnectionString(caller: string): string { return localize('schemaCompare.GetConnectionString', "Do you want to connect to {0}?", caller); }
export const selectConnection: string = localize('schemaCompare.selectConnection', "Select connection");
// object types
export const Aggregates: string = localize('SchemaCompare.Aggregates', "Aggregates");
export const ApplicationRoles: string = localize('SchemaCompare.ApplicationRoles', "Application Roles");
export const Assemblies: string = localize('SchemaCompare.Assemblies', "Assemblies");
export const AssemblyFiles: string = localize('SchemaCompare.AssemblyFiles', "Assembly Files");
export const AsymmetricKeys: string = localize('SchemaCompare.AsymmetricKeys', "Asymmetric Keys");
export const BrokerPriorities: string = localize('SchemaCompare.BrokerPriorities', "Broker Priorities");
export const Certificates: string = localize('SchemaCompare.Certificates', "Certificates");
export const ColumnEncryptionKeys: string = localize('SchemaCompare.ColumnEncryptionKeys', "Column Encryption Keys");
export const ColumnMasterKeys: string = localize('SchemaCompare.ColumnMasterKeys', "Column Master Keys");
export const Contracts: string = localize('SchemaCompare.Contracts', "Contracts");
export const DatabaseOptions: string = localize('SchemaCompare.DatabaseOptions', "Database Options");
export const DatabaseRoles: string = localize('SchemaCompare.DatabaseRoles', "Database Roles");
export const DatabaseTriggers: string = localize('SchemaCompare.DatabaseTriggers', "Database Triggers");
export const Defaults: string = localize('SchemaCompare.Defaults', "Defaults");
export const ExtendedProperties: string = localize('SchemaCompare.ExtendedProperties', "Extended Properties");
export const ExternalDataSources: string = localize('SchemaCompare.ExternalDataSources', "External Data Sources");
export const ExternalFileFormats: string = localize('SchemaCompare.ExternalFileFormats', "External File Formats");
export const ExternalStreams: string = localize('SchemaCompare.ExternalStreams', "External Streams");
export const ExternalStreamingJobs: string = localize('SchemaCompare.ExternalStreamingJobs', "External Streaming Jobs");
export const ExternalTables: string = localize('SchemaCompare.ExternalTables', "External Tables");
export const Filegroups: string = localize('SchemaCompare.Filegroups', "Filegroups");
export const Files: string = localize('SchemaCompare.Files', "Files");
export const FileTables: string = localize('SchemaCompare.FileTables', "File Tables");
export const FullTextCatalogs: string = localize('SchemaCompare.FullTextCatalogs', "Full Text Catalogs");
export const FullTextStoplists: string = localize('SchemaCompare.FullTextStoplists', "Full Text Stoplists");
export const MessageTypes: string = localize('SchemaCompare.MessageTypes', "Message Types");
export const PartitionFunctions: string = localize('SchemaCompare.PartitionFunctions', "Partition Functions");
export const PartitionSchemes: string = localize('SchemaCompare.PartitionSchemes', "Partition Schemes");
export const Permissions: string = localize('SchemaCompare.Permissions', "Permissions");
export const Queues: string = localize('SchemaCompare.Queues', "Queues");
export const RemoteServiceBindings: string = localize('SchemaCompare.RemoteServiceBindings', "Remote Service Bindings");
export const RoleMembership: string = localize('SchemaCompare.RoleMembership', "Role Membership");
export const Rules: string = localize('SchemaCompare.Rules', "Rules");
export const ScalarValuedFunctions: string = localize('SchemaCompare.ScalarValuedFunctions', "Scalar Valued Functions");
export const SearchPropertyLists: string = localize('SchemaCompare.SearchPropertyLists', "Search Property Lists");
export const SecurityPolicies: string = localize('SchemaCompare.SecurityPolicies', "Security Policies");
export const Sequences: string = localize('SchemaCompare.Sequences', "Sequences");
export const Services: string = localize('SchemaCompare.Services', "Services");
export const Signatures: string = localize('SchemaCompare.Signatures', "Signatures");
export const StoredProcedures: string = localize('SchemaCompare.StoredProcedures', "Stored Procedures");
export const SymmetricKeys: string = localize('SchemaCompare.SymmetricKeys', "Symmetric Keys");
export const Synonyms: string = localize('SchemaCompare.Synonyms', "Synonyms");
export const Tables: string = localize('SchemaCompare.Tables', "Tables");
export const TableValuedFunctions: string = localize('SchemaCompare.TableValuedFunctions', "Table Valued Functions");
export const UserDefinedDataTypes: string = localize('SchemaCompare.UserDefinedDataTypes', "User Defined Data Types");
export const UserDefinedTableTypes: string = localize('SchemaCompare.UserDefinedTableTypes', "User Defined Table Types");
export const ClrUserDefinedTypes: string = localize('SchemaCompare.ClrUserDefinedTypes', "Clr User Defined Types");
export const Users: string = localize('SchemaCompare.Users', "Users");
export const Views: string = localize('SchemaCompare.Views', "Views");
export const XmlSchemaCollections: string = localize('SchemaCompare.XmlSchemaCollections', "Xml Schema Collections");
export const Audits: string = localize('SchemaCompare.Audits', "Audits");
export const Credentials: string = localize('SchemaCompare.Credentials', "Credentials");
export const CryptographicProviders: string = localize('SchemaCompare.CryptographicProviders', "Cryptographic Providers");
export const DatabaseAuditSpecifications: string = localize('SchemaCompare.DatabaseAuditSpecifications', "Database Audit Specifications");
export const DatabaseEncryptionKeys: string = localize('SchemaCompare.DatabaseEncryptionKeys', "Database Encryption Keys");
export const DatabaseScopedCredentials: string = localize('SchemaCompare.DatabaseScopedCredentials', "Database Scoped Credentials");
export const Endpoints: string = localize('SchemaCompare.Endpoints', "Endpoints");
export const ErrorMessages: string = localize('SchemaCompare.ErrorMessages', "Error Messages");
export const EventNotifications: string = localize('SchemaCompare.EventNotifications', "Event Notifications");
export const EventSessions: string = localize('SchemaCompare.EventSessions', "Event Sessions");
export const LinkedServerLogins: string = localize('SchemaCompare.LinkedServerLogins', "Linked Server Logins");
export const LinkedServers: string = localize('SchemaCompare.LinkedServers', "Linked Servers");
export const Logins: string = localize('SchemaCompare.Logins', "Logins");
export const MasterKeys: string = localize('SchemaCompare.MasterKeys', "Master Keys");
export const Routes: string = localize('SchemaCompare.Routes', "Routes");
export const ServerAuditSpecifications: string = localize('SchemaCompare.ServerAuditSpecifications', "Server Audit Specifications");
export const ServerRoleMembership: string = localize('SchemaCompare.ServerRoleMembership', "Server Role Membership");
export const ServerRoles: string = localize('SchemaCompare.ServerRoles', "Server Roles");
export const ServerTriggers: string = localize('SchemaCompare.ServerTriggers', "Server Triggers");
// Error messages
export function compareErrorMessage(errorMessage: string): string { return localize('schemaCompare.compareErrorMessage', "Schema Compare failed: {0}", errorMessage ? errorMessage : 'Unknown'); }
export function saveScmpErrorMessage(errorMessage: string): string { return localize('schemaCompare.saveScmpErrorMessage', "Save scmp failed: '{0}'", (errorMessage) ? errorMessage : 'Unknown'); }

View File

@@ -5,16 +5,15 @@
import * as loc from '../localizedConstants';
import * as mssql from 'mssql';
import * as vscode from 'vscode';
import { isNullOrUndefined } from 'util';
export class SchemaCompareOptionsModel {
// key is the option display name and values are checkboxValue and optionName
private optionsValueNameLookup: { [key: string]: mssql.IOptionWithValue } = {};
public excludedObjectTypes: number[] = [];
public objectsLookup = {};
private includeObjectTypesLookup: { [key: string]: mssql.IOptionWithValue } = {};
constructor(public deploymentOptions: mssql.DeploymentOptions) {
this.setOptionsToValueNameLookup();
this.setIncludeObjectTypesLookup();
}
/*
@@ -72,590 +71,62 @@ export class SchemaCompareOptionsModel {
return optionName !== undefined ? this.deploymentOptions.booleanOptionsDictionary[optionName.optionName].description : '';
}
//#region Schema Compare Objects
public objectTypeLabels: string[] = [
loc.Aggregates,
loc.ApplicationRoles,
loc.Assemblies,
loc.AssemblyFiles,
loc.AsymmetricKeys,
loc.BrokerPriorities,
loc.Certificates,
loc.ColumnEncryptionKeys,
loc.ColumnMasterKeys,
loc.Contracts,
loc.DatabaseOptions,
loc.DatabaseRoles,
loc.DatabaseTriggers,
loc.Defaults,
loc.ExtendedProperties,
loc.ExternalDataSources,
loc.ExternalFileFormats,
loc.ExternalStreams,
loc.ExternalStreamingJobs,
loc.ExternalTables,
loc.Filegroups,
loc.Files,
loc.FileTables,
loc.FullTextCatalogs,
loc.FullTextStoplists,
loc.MessageTypes,
loc.PartitionFunctions,
loc.PartitionSchemes,
loc.Permissions,
loc.Queues,
loc.RemoteServiceBindings,
loc.RoleMembership,
loc.Rules,
loc.ScalarValuedFunctions,
loc.SearchPropertyLists,
loc.SecurityPolicies,
loc.Sequences,
loc.Services,
loc.Signatures,
loc.StoredProcedures,
loc.SymmetricKeys,
loc.Synonyms,
loc.Tables,
loc.TableValuedFunctions,
loc.UserDefinedDataTypes,
loc.UserDefinedTableTypes,
loc.ClrUserDefinedTypes,
loc.Users,
loc.Views,
loc.XmlSchemaCollections,
loc.Audits,
loc.Credentials,
loc.CryptographicProviders,
loc.DatabaseAuditSpecifications,
loc.DatabaseEncryptionKeys,
loc.DatabaseScopedCredentials,
loc.Endpoints,
loc.ErrorMessages,
loc.EventNotifications,
loc.EventSessions,
loc.LinkedServerLogins,
loc.LinkedServers,
loc.Logins,
loc.MasterKeys,
loc.Routes,
loc.ServerAuditSpecifications,
loc.ServerRoleMembership,
loc.ServerRoles,
loc.ServerTriggers
].sort();
public getObjectsData(): string[][] {
let data = [];
this.objectsLookup = {};
this.objectTypeLabels.forEach(l => {
let checked: boolean = this.getSchemaCompareIncludedObjectsUtil(l);
data.push([checked, l]);
this.objectsLookup[l] = checked;
/*
* Sets include object types option's checkbox values and property name to the includeObjectTypesLookup map
*/
public setIncludeObjectTypesLookup(): void {
Object.entries(this.deploymentOptions.objectTypesDictionary).forEach(option => {
const optionValue: mssql.IOptionWithValue = {
optionName: option[0],
checked: this.getIncludeObjectTypeOptionCheckStatus(option[0])
};
this.includeObjectTypesLookup[option[1]] = optionValue;
});
return data;
}
//#endregion
public setObjectTypeOptions() {
for (let option in this.objectsLookup) {
this.setSchemaCompareIncludedObjectsUtil(option, this.objectsLookup[option]);
}
this.deploymentOptions.excludeObjectTypes.value = this.excludedObjectTypes;
}
public getSchemaCompareIncludedObjectsUtil(label): boolean {
switch (label) {
case loc.Aggregates:
return !isNullOrUndefined(this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Aggregates)) ? false : true;
case loc.ApplicationRoles:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ApplicationRoles)) ? false : true;
case loc.Assemblies:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Assemblies)) ? false : true;
case loc.AssemblyFiles:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.AssemblyFiles)) ? false : true;
case loc.AsymmetricKeys:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.AsymmetricKeys)) ? false : true;
case loc.BrokerPriorities:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.BrokerPriorities)) ? false : true;
case loc.Certificates:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Certificates)) ? false : true;
case loc.ColumnEncryptionKeys:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ColumnEncryptionKeys)) ? false : true;
case loc.ColumnMasterKeys:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ColumnMasterKeys)) ? false : true;
case loc.Contracts:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Contracts)) ? false : true;
case loc.DatabaseOptions:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.DatabaseOptions)) ? false : true;
case loc.DatabaseRoles:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.DatabaseRoles)) ? false : true;
case loc.DatabaseTriggers:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.DatabaseTriggers)) ? false : true;
case loc.Defaults:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Defaults)) ? false : true;
case loc.ExtendedProperties:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ExtendedProperties)) ? false : true;
case loc.ExternalDataSources:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ExternalDataSources)) ? false : true;
case loc.ExternalFileFormats:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ExternalFileFormats)) ? false : true;
case loc.ExternalStreams:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ExternalStreams)) ? false : true;
case loc.ExternalStreamingJobs:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ExternalStreamingJobs)) ? false : true;
case loc.ExternalTables:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ExternalTables)) ? false : true;
case loc.Filegroups:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Filegroups)) ? false : true;
case loc.Files:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Files)) ? false : true;
case loc.FileTables:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.FileTables)) ? false : true;
case loc.FullTextCatalogs:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.FullTextCatalogs)) ? false : true;
case loc.FullTextStoplists:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.FullTextStoplists)) ? false : true;
case loc.MessageTypes:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.MessageTypes)) ? false : true;
case loc.PartitionFunctions:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.PartitionFunctions)) ? false : true;
case loc.PartitionSchemes:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.PartitionSchemes)) ? false : true;
case loc.Permissions:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Permissions)) ? false : true;
case loc.Queues:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Queues)) ? false : true;
case loc.RemoteServiceBindings:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.RemoteServiceBindings)) ? false : true;
case loc.RoleMembership:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.RoleMembership)) ? false : true;
case loc.Rules:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Rules)) ? false : true;
case loc.ScalarValuedFunctions:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ScalarValuedFunctions)) ? false : true;
case loc.SearchPropertyLists:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.SearchPropertyLists)) ? false : true;
case loc.SecurityPolicies:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.SecurityPolicies)) ? false : true;
case loc.Sequences:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Sequences)) ? false : true;
case loc.Services:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Services)) ? false : true;
case loc.Signatures:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Signatures)) ? false : true;
case loc.StoredProcedures:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.StoredProcedures)) ? false : true;
case loc.SymmetricKeys:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.SymmetricKeys)) ? false : true;
case loc.Synonyms:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Synonyms)) ? false : true;
case loc.Tables:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Tables)) ? false : true;
case loc.TableValuedFunctions:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.TableValuedFunctions)) ? false : true;
case loc.UserDefinedDataTypes:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.UserDefinedDataTypes)) ? false : true;
case loc.UserDefinedTableTypes:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.UserDefinedTableTypes)) ? false : true;
case loc.ClrUserDefinedTypes:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ClrUserDefinedTypes)) ? false : true;
case loc.Users:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Users)) ? false : true;
case loc.Views:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Views)) ? false : true;
case loc.XmlSchemaCollections:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.XmlSchemaCollections)) ? false : true;
case loc.Audits:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Audits)) ? false : true;
case loc.Credentials:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Credentials)) ? false : true;
case loc.CryptographicProviders:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.CryptographicProviders)) ? false : true;
case loc.DatabaseAuditSpecifications:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.DatabaseAuditSpecifications)) ? false : true;
case loc.DatabaseEncryptionKeys:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.DatabaseEncryptionKeys)) ? false : true;
case loc.DatabaseScopedCredentials:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.DatabaseScopedCredentials)) ? false : true;
case loc.Endpoints:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Endpoints)) ? false : true;
case loc.ErrorMessages:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ErrorMessages)) ? false : true;
case loc.EventNotifications:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.EventNotifications)) ? false : true;
case loc.EventSessions:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.EventSessions)) ? false : true;
case loc.LinkedServerLogins:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.LinkedServerLogins)) ? false : true;
case loc.LinkedServers:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.LinkedServers)) ? false : true;
case loc.Logins:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Logins)) ? false : true;
case loc.MasterKeys:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.MasterKeys)) ? false : true;
case loc.Routes:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Routes)) ? false : true;
case loc.ServerAuditSpecifications:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ServerAuditSpecifications)) ? false : true;
case loc.ServerRoleMembership:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ServerRoleMembership)) ? false : true;
case loc.ServerRoles:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ServerRoles)) ? false : true;
case loc.ServerTriggers:
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ServerTriggers)) ? false : true;
}
return false;
/*
* Initialize options data from include objects options for table component
* Returns data as [booleanValue, optionName]
*/
public getIncludeObjectTypesOptionsData(): any[][] {
let data: any[][] = [];
Object.entries(this.deploymentOptions.objectTypesDictionary).forEach(option => {
// option[1] is the display name and option[0] is the optionName
data.push([this.getIncludeObjectTypeOptionCheckStatus(option[0]), option[1]]);
});
return data.sort((a, b) => a[1].localeCompare(b[1]));
}
public setSchemaCompareIncludedObjectsUtil(label: string, included: boolean) {
switch (label) {
case loc.Aggregates:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Aggregates);
}
return;
case loc.ApplicationRoles:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.ApplicationRoles);
}
return;
case loc.Assemblies:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Assemblies);
}
return;
case loc.AssemblyFiles:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.AssemblyFiles);
}
return;
case loc.AsymmetricKeys:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.AsymmetricKeys);
}
return;
case loc.BrokerPriorities:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.BrokerPriorities);
}
return;
case loc.Certificates:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Certificates);
}
return;
case loc.ColumnEncryptionKeys:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.ColumnEncryptionKeys);
}
return;
case loc.ColumnMasterKeys:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.ColumnMasterKeys);
}
return;
case loc.Contracts:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Contracts);
}
return;
case loc.DatabaseOptions:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.DatabaseOptions);
}
return;
case loc.DatabaseRoles:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.DatabaseRoles);
}
return;
case loc.DatabaseTriggers:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.DatabaseTriggers);
}
return;
case loc.Defaults:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Defaults);
}
return;
case loc.ExtendedProperties:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.ExtendedProperties);
}
return;
case loc.ExternalDataSources:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.ExternalDataSources);
}
return;
case loc.ExternalFileFormats:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.ExternalFileFormats);
}
return;
case loc.ExternalStreams:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.ExternalStreams);
}
return;
case loc.ExternalStreamingJobs:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.ExternalStreamingJobs);
}
return;
case loc.ExternalTables:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.ExternalTables);
}
return;
case loc.Filegroups:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Filegroups);
}
return;
case loc.Files:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Files);
}
return;
case loc.FileTables:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.FileTables);
}
return;
case loc.FullTextCatalogs:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.FullTextCatalogs);
}
return;
case loc.FullTextStoplists:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.FullTextStoplists);
}
return;
case loc.MessageTypes:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.MessageTypes);
}
return;
case loc.PartitionFunctions:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.PartitionFunctions);
}
return;
case loc.PartitionSchemes:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.PartitionSchemes);
}
return;
case loc.Permissions:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Permissions);
}
return;
case loc.Queues:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Queues);
}
return;
case loc.RemoteServiceBindings:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.RemoteServiceBindings);
}
return;
case loc.RoleMembership:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.RoleMembership);
}
return;
case loc.Rules:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Rules);
}
return;
case loc.ScalarValuedFunctions:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.ScalarValuedFunctions);
}
return;
case loc.SearchPropertyLists:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.SearchPropertyLists);
}
return;
case loc.SecurityPolicies:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.SecurityPolicies);
}
return;
case loc.Sequences:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Sequences);
}
return;
case loc.Services:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Services);
}
return;
case loc.Signatures:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Signatures);
}
return;
case loc.StoredProcedures:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.StoredProcedures);
}
return;
case loc.SymmetricKeys:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.SymmetricKeys);
}
return;
case loc.Synonyms:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Synonyms);
}
return;
case loc.Tables:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Tables);
}
return;
case loc.TableValuedFunctions:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.TableValuedFunctions);
}
return;
case loc.UserDefinedDataTypes:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.UserDefinedDataTypes);
}
return;
case loc.UserDefinedTableTypes:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.UserDefinedTableTypes);
}
return;
case loc.ClrUserDefinedTypes:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.ClrUserDefinedTypes);
}
return;
case loc.Users:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Users);
}
return;
case loc.Views:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Views);
}
return;
case loc.XmlSchemaCollections:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.XmlSchemaCollections);
}
return;
case loc.Audits:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Audits);
}
return;
case loc.Credentials:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Credentials);
}
return;
case loc.CryptographicProviders:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.CryptographicProviders);
}
return;
case loc.DatabaseAuditSpecifications:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.DatabaseAuditSpecifications);
}
return;
case loc.DatabaseEncryptionKeys:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.DatabaseEncryptionKeys);
}
return;
case loc.DatabaseScopedCredentials:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.DatabaseScopedCredentials);
}
return;
case loc.Endpoints:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Endpoints);
}
return;
case loc.ErrorMessages:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.ErrorMessages);
}
return;
case loc.EventNotifications:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.EventNotifications);
}
return;
case loc.EventSessions:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.EventSessions);
}
return;
case loc.LinkedServerLogins:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.LinkedServerLogins);
}
return;
case loc.LinkedServers:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.LinkedServers);
}
return;
case loc.Logins:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Logins);
}
return;
case loc.MasterKeys:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.MasterKeys);
}
return;
case loc.Routes:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.Routes);
}
return;
case loc.ServerAuditSpecifications:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.ServerAuditSpecifications);
}
return;
case loc.ServerRoleMembership:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.ServerRoleMembership);
}
return;
case loc.ServerRoles:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.ServerRoles);
}
return;
case loc.ServerTriggers:
if (!included) {
this.excludedObjectTypes.push(mssql.SchemaObjectType.ServerTriggers);
}
return;
}
/*
* Gets the selected/default value of the object type option
* return false for the deploymentOptions.excludeObjectTypes option, if it is in ObjectTypesDictionary
*/
public getIncludeObjectTypeOptionCheckStatus(optionName: string): boolean {
return (this.deploymentOptions.excludeObjectTypes.value?.find(x => x.toLowerCase() === optionName.toLowerCase())) !== undefined ? false : true;
}
/*
* Sets the checkbox value to the includeObjectTypesLookup map
*/
public setIncludeObjectTypesOptionValue(displayName: string, checked: boolean): void {
this.includeObjectTypesLookup[displayName].checked = checked;
}
/*
* Sets the selected option checkbox value to the deployment options
*/
public setIncludeObjectTypesToDeploymentOptions(): void {
let finalExcludedObjectTypes: string[] = [];
Object.entries(this.includeObjectTypesLookup).forEach(option => {
// option[1] holds checkedbox value and optionName
// sending the unchecked(false) options only to the excludeObjectTypes
if (!option[1].checked) {
finalExcludedObjectTypes.push(option[1].optionName);
}
});
this.deploymentOptions.excludeObjectTypes.value = finalExcludedObjectTypes;
}
}

View File

@@ -1126,6 +1126,20 @@ export class SchemaCompareMainWindow {
if (ownerUri) {
endpointInfo = endpoint;
endpointInfo.ownerUri = ownerUri;
} else if (endpoint.endpointType === mssql.SchemaCompareEndpointType.Project) {
endpointInfo = {
endpointType: endpoint.endpointType,
packageFilePath: '',
serverDisplayName: '',
serverName: '',
databaseName: '',
ownerUri: '',
connectionDetails: undefined,
projectFilePath: endpoint.projectFilePath,
targetScripts: [],
dataSchemaProvider: '',
folderStructure: loc.schemaObjectType // TODO: Pick this automatically from the scmp file, after issue #20332 is resolved (check dsp as well)
};
} else {
// need to do this instead of just setting it to the endpoint because some fields are null which will cause an error when sending the compare request
endpointInfo = {

View File

@@ -11,30 +11,42 @@ describe('Schema Compare Options Model', () => {
it('Should create model and set options successfully', function (): void {
const model = new SchemaCompareOptionsModel(testUtils.getDeploymentOptions());
should.notEqual(model.getOptionsData(), undefined, 'Options shouldn\'t be undefined');
should.notEqual(model.getObjectsData(), undefined, 'Objects shouldn\'t be undefined');
should.notEqual(model.getIncludeObjectTypesOptionsData(), undefined, 'Objects shouldn\'t be undefined');
should.doesNotThrow(() => model.setDeploymentOptions());
should.doesNotThrow(() => model.setObjectTypeOptions());
should(model.getSchemaCompareIncludedObjectsUtil('')).be.false('Should return false if invalid object name is passed in');
should.doesNotThrow(() => model.setIncludeObjectTypesToDeploymentOptions());
});
it('Should exclude objects', function (): void {
it('Should not have a default object types to exclude from IncludeObjectTypes ', function (): void {
const model = new SchemaCompareOptionsModel(testUtils.getDeploymentOptions());
should(model.excludedObjectTypes.length).be.equal(0, 'There should be no excluded objects');
should(model.deploymentOptions.excludeObjectTypes.value.length).be.equal(0, 'There should be no object type excluded from IncludeObjectTypes');
model.objectTypeLabels.forEach(label => {
model.setSchemaCompareIncludedObjectsUtil(label, false);
Object.keys(model.deploymentOptions.objectTypesDictionary).forEach(option => {
should(model.getIncludeObjectTypeOptionCheckStatus(option)).equal(true, 'Object types that are not excluded should return true');
});
});
should(model.excludedObjectTypes.length).be.equal(model.objectTypeLabels.length, 'All the object types should be excluded');
it('Should have default object types to exclude from IncludeObjectTypes ', function (): void {
const model = new SchemaCompareOptionsModel(testUtils.getDeploymentOptions());
model.deploymentOptions.excludeObjectTypes.value = ['SampleProperty1'];
should(model.deploymentOptions.excludeObjectTypes.value.length).be.equal(1, 'There should be one object type excluding from IncludeObjectTypes ');
// should return false for the default object types and false for the remaining object types
Object.keys(model.deploymentOptions.objectTypesDictionary).forEach(option => {
if (option === 'SampleProperty1') {
should(model.getIncludeObjectTypeOptionCheckStatus(option)).equal(false, 'Object type property that have default object types to exclude from IncludeObjectTypes should return false');
} else {
should(model.getIncludeObjectTypeOptionCheckStatus(option)).equal(true, 'All including Object type should return true');
}
});
});
it('Should get descriptions', function (): void {
const model = new SchemaCompareOptionsModel(testUtils.getDeploymentOptions());
model.getOptionsData();
Object.entries(model.deploymentOptions.booleanOptionsDictionary).forEach(option => {
should(model.getOptionDescription(option[1].displayName)).not.equal(undefined);
should(model.getOptionDescription(option[1].displayName)).not.equal(undefined, 'Option description shouldn\'t be undefined');
});
});

View File

@@ -117,6 +117,10 @@ export function getDeploymentOptions(): mssql.DeploymentOptions {
booleanOptionsDictionary: {
'SampleDisplayOption1': { value: false, description: sampleDesc, displayName: sampleName },
'SampleDisplayOption2': { value: false, description: sampleDesc, displayName: sampleName }
},
objectTypesDictionary: {
'SampleProperty1': sampleName,
'SampleProperty2': sampleName
}
};
}

View File

@@ -14,6 +14,7 @@ import { ConnectionDetails, IConnectionInfo } from 'vscode-mssql';
import { AzureFunctionsExtensionApi } from '../../../types/vscode-azurefunctions.api';
// https://github.com/microsoft/vscode-azuretools/blob/main/ui/api.d.ts
import { AzureExtensionApiProvider } from '../../../types/vscode-azuretools.api';
import { TelemetryActions, TelemetryReporter, TelemetryViews } from './telemetry';
/**
* Represents the settings in an Azure function project's locawl.settings.json file
*/
@@ -24,10 +25,7 @@ export interface ILocalSettingsJson {
ConnectionStrings?: { [key: string]: string };
}
export interface IFileFunctionObject {
filePromise: Promise<string>;
watcherDisposable: vscode.Disposable;
}
export const outputChannel = vscode.window.createOutputChannel(constants.serviceName);
/**
* copied and modified from vscode-azurefunctions extension
@@ -198,47 +196,16 @@ export async function getSettingsFile(projectFolder: string): Promise<string | u
}
/**
* New azure function file watcher and watcher disposable to be used to watch for changes to the azure function project
* @param projectFolder is the parent directory to the project file
* @returns the function file path once created and the watcher disposable
* Adds the latest SQL nuget package to the project
* @param projectFolder is the folder containing the project file
*/
export function waitForNewFunctionFile(projectFolder: string): IFileFunctionObject {
const watcher = vscode.workspace.createFileSystemWatcher((
new vscode.RelativePattern(projectFolder, '**/*.cs')), false, true, true);
const filePromise = new Promise<string>((resolve, _) => {
watcher.onDidCreate((e) => {
resolve(e.fsPath);
});
});
return {
filePromise,
watcherDisposable: watcher
};
}
/**
* Retrieves the new host project file once it has created and the watcher disposable
* @returns the host file path once created and the watcher disposable
*/
export function waitForNewHostFile(): IFileFunctionObject {
const watcher = vscode.workspace.createFileSystemWatcher('**/host.json', false, true, true);
const filePromise = new Promise<string>((resolve, _) => {
watcher.onDidCreate((e) => {
resolve(e.fsPath);
});
});
return {
filePromise,
watcherDisposable: watcher
};
}
/**
* Adds the required nuget package to the project
* @param selectedProjectFile is the users selected project file path
*/
export async function addSqlNugetReferenceToProjectFile(selectedProjectFile: string): Promise<void> {
await utils.executeCommand(`dotnet add "${selectedProjectFile}" package ${constants.sqlExtensionPackageName} --prerelease`);
export async function addSqlNugetReferenceToProjectFile(projectFolder: string): Promise<void> {
// clear the output channel prior to adding the nuget reference
outputChannel.clear();
let addNugetCommmand = await utils.executeCommand(`dotnet add "${projectFolder}" package ${constants.sqlExtensionPackageName} --prerelease`);
outputChannel.appendLine(constants.dotnetResult(addNugetCommmand));
outputChannel.show(true);
TelemetryReporter.sendActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.addSQLNugetPackage);
}
/**
@@ -510,8 +477,6 @@ export async function promptAndUpdateConnectionStringSetting(projectUri: vscode.
connectionStringSettingName = selectedSetting?.label;
}
}
// Add sql extension package reference to project. If the reference is already there, it doesn't get added again
await addSqlNugetReferenceToProjectFile(projectUri.fsPath);
} else {
// if no AF project was found or there's more than one AF functions project in the workspace,
// ask for the user to input the setting name
@@ -617,19 +582,9 @@ export async function promptSelectDatabase(connectionURI: string): Promise<strin
export async function getConnectionURI(connectionInfo: IConnectionInfo): Promise<string | undefined> {
const vscodeMssqlApi = await utils.getVscodeMssqlApi();
let connectionURI: string = '';
try {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: constants.connectionProgressTitle,
cancellable: false
}, async (_progress, _token) => {
// show progress bar while connecting to the users selected connection profile
connectionURI = await vscodeMssqlApi.connect(connectionInfo!);
}
);
connectionURI = await vscodeMssqlApi.connect(connectionInfo);
} catch (e) {
// mssql connection error will be shown to the user
return undefined;

View File

@@ -22,6 +22,7 @@ export const sqlBindingsHelpLink = 'https://github.com/Azure/azure-functions-sql
export const passwordPlaceholder = '******';
export const azureFunctionLocalSettingsFileName = 'local.settings.json';
export const vscodeOpenCommand = 'vscode.open';
export const serviceName = 'SQL Bindings';
// localized constants
export const functionNameTitle = localize('functionNameTitle', 'Function Name');
@@ -73,7 +74,6 @@ export const userPasswordLater = localize('userPasswordLater', 'In order to user
export const openFile = localize('openFile', "Open File");
export const closeButton = localize('closeButton', "Close");
export const enterPasswordPrompt = localize('enterPasswordPrompt', '(Optional) Enter connection password to save in local.settings.json');
export const connectionProgressTitle = localize('connectionProgressTitle', "Testing SQL Server connection...");
export const enterTableName = localize('enterTableName', 'Enter SQL table to query');
export const enterViewName = localize('enterViewName', 'Enter SQL view to query');
export const enterTableNameToUpsert = localize('enterTableNameToUpsert', 'Enter SQL table to upsert into');
@@ -91,6 +91,7 @@ export function failedToParse(filename: string, error: any): string { return loc
export function addSqlBinding(functionName: string): string { return localize('addSqlBinding', 'Adding SQL Binding to function "{0}"...'), functionName; }
export function errorNewAzureFunction(error: any): string { return localize('errorNewAzureFunction', 'Error creating new Azure Function: {0}', utils.getErrorMessage(error)); }
export function manuallyEnterObjectName(userObjectName: string): string { return `$(pencil) ${userObjectName}`; }
export function dotnetResult(output: string): string { return localize('dotnetResult', 'Adding SQL nuget package:\n{0}', output); }
// Known Azure settings reference for Azure Functions
// https://docs.microsoft.com/en-us/azure/azure-functions/functions-app-settings

View File

@@ -11,7 +11,8 @@ export const TelemetryReporter = new AdsTelemetryReporter(packageInfo.name, pack
export enum TelemetryViews {
SqlBindingsQuickPick = 'SqlBindingsQuickPick',
CreateAzureFunctionWithSqlBinding = 'CreateAzureFunctionWithSqlBinding'
CreateAzureFunctionWithSqlBinding = 'CreateAzureFunctionWithSqlBinding',
AzureFunctionsUtils = 'AzureFunctionsUtils',
}
export enum TelemetryActions {
@@ -28,9 +29,13 @@ export enum TelemetryActions {
updateConnectionString = 'updateConnectionString',
finishAddSqlBinding = 'finishAddSqlBinding',
exitSqlBindingsQuickpick = 'exitSqlBindingsQuickpick',
// Azure Functions Utils
addSQLNugetPackage = 'addSQLNugetPackage',
}
export enum CreateAzureFunctionStep {
noAzureFunctionsExtension = 'noAzureFunctionsExtension',
getAzureFunctionProject = 'getAzureFunctionProject',
learnMore = 'learnMore',
helpCreateAzureFunctionProject = 'helpCreateAzureFunctionProject',

View File

@@ -83,20 +83,6 @@ export function generateQuotedFullName(schema: string, objectName: string): stri
return `[${escapeClosingBrackets(schema)}].[${escapeClosingBrackets(objectName)}]`;
}
/**
* Returns a promise that will reject after the specified timeout
* @param errorMessage error message to be returned in the rejection
* @param ms timeout in milliseconds. Default is 10 seconds
* @returns a promise that rejects after the specified timeout
*/
export function timeoutPromise(errorMessage: string, ms: number = 10000): Promise<string> {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new TimeoutError(errorMessage));
}, ms);
});
}
/**
* Gets a unique file name
* Increment the file name by adding 1 to function name if the file already exists

View File

@@ -7,6 +7,7 @@ import * as vscode from 'vscode';
import * as uuid from 'uuid';
import * as constants from '../common/constants';
import * as utils from '../common/utils';
import * as path from 'path';
import * as azureFunctionsUtils from '../common/azureFunctionsUtils';
import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
import { addSqlBinding, getAzureFunctions } from '../services/azureFunctionsService';
@@ -113,6 +114,13 @@ export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined):
.withAdditionalProperties(propertyBag).send();
return;
}
// Add latest sql extension package reference to project
// only add if AF project (.csproj) is found
if (projectUri?.fsPath) {
await azureFunctionsUtils.addSqlNugetReferenceToProjectFile(path.dirname(projectUri.fsPath));
}
exitReason = 'done';
TelemetryReporter.createActionEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.finishAddSqlBinding)
.withAdditionalProperties(propertyBag).send();

View File

@@ -26,14 +26,15 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
TelemetryReporter.sendActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.startCreateAzureFunctionWithSqlBinding);
let connectionInfo: IConnectionInfo | undefined;
let isCreateNewProject: boolean = false;
let newFunctionFileObject: azureFunctionsUtils.IFileFunctionObject | undefined;
try {
// check to see if Azure Functions Extension is installed
const azureFunctionApi = await azureFunctionsUtils.getAzureFunctionsExtensionApi();
if (!azureFunctionApi) {
exitReason = ExitReason.error;
propertyBag.exitReason = exitReason;
TelemetryReporter.createErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick)
telemetryStep = CreateAzureFunctionStep.noAzureFunctionsExtension;
TelemetryReporter.createErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
.withAdditionalProperties(propertyBag).send();
return;
}
@@ -70,6 +71,7 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
{ title: constants.selectAzureFunctionProjFolder, ignoreFocusOut: true });
if (!browseProjectLocation) {
// User cancelled
exitReason = ExitReason.cancelled;
return;
}
const projectFolders = (await vscode.window.showOpenDialog({
@@ -80,6 +82,7 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
}));
if (!projectFolders) {
// User cancelled
exitReason = ExitReason.cancelled;
return;
}
projectFolder = projectFolders[0].fsPath;
@@ -89,6 +92,7 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
break;
} else {
// user cancelled
exitReason = ExitReason.cancelled;
return;
}
}
@@ -96,9 +100,6 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
// user has an azure function project open
projectFolder = path.dirname(projectFile);
}
// create a system file watcher for the project folder
newFunctionFileObject = azureFunctionsUtils.waitForNewFunctionFile(projectFolder);
// Get connection string parameters and construct object name from prompt or connectionInfo given
let objectName: string | undefined;
let selectedBindingType: BindingType | undefined;
@@ -109,6 +110,7 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
let chosenObjectType = await azureFunctionsUtils.promptForObjectType();
if (!chosenObjectType) {
// User cancelled
exitReason = ExitReason.cancelled;
return;
}
@@ -116,6 +118,8 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
telemetryStep = CreateAzureFunctionStep.getBindingType;
selectedBindingType = await azureFunctionsUtils.promptForBindingType(chosenObjectType);
if (!selectedBindingType) {
// User cancelled
exitReason = ExitReason.cancelled;
return;
}
@@ -137,6 +141,7 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
}
if (!connectionInfo) {
// User cancelled
exitReason = ExitReason.cancelled;
return;
}
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
@@ -174,6 +179,8 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
let nodeType = ObjectType.Table === node.nodeType ? ObjectType.Table : ObjectType.View;
selectedBindingType = await azureFunctionsUtils.promptForBindingType(nodeType);
if (!selectedBindingType) {
// User cancelled
exitReason = ExitReason.cancelled;
return;
}
@@ -201,6 +208,8 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
validateInput: input => utils.validateFunctionName(input)
}) as string;
if (!functionName) {
// User cancelled
exitReason = ExitReason.cancelled;
return;
}
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
@@ -219,6 +228,7 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
connectionStringInfo = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(vscode.Uri.parse(projectFile), connectionInfo);
if (!connectionStringInfo) {
// User cancelled connection string setting name prompt or connection string method prompt
exitReason = ExitReason.cancelled;
return;
}
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
@@ -245,13 +255,14 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
suppressCreateProjectPrompt: true,
...(isCreateNewProject && { executeStep: connectionStringExecuteStep })
});
// Add latest sql extension package reference to project
await azureFunctionsUtils.addSqlNugetReferenceToProjectFile(projectFolder);
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
.withAdditionalProperties(propertyBag)
.withConnectionInfo(connectionInfo).send();
// check for the new function file to be created and dispose of the file system watcher
const timeoutForFunctionFile = utils.timeoutPromise(constants.timeoutAzureFunctionFileError);
await Promise.race([newFunctionFileObject.filePromise, timeoutForFunctionFile]);
telemetryStep = 'finishCreateFunction';
propertyBag.telemetryStep = telemetryStep;
exitReason = ExitReason.finishCreate;
@@ -261,15 +272,9 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
} catch (error) {
let errorType = utils.getErrorType(error);
propertyBag.telemetryStep = telemetryStep;
if (errorType === 'TimeoutError') {
// this error can be cause by many different scenarios including timeout or error occurred during createFunction
exitReason = ExitReason.timeout;
console.log('Timed out waiting for Azure Function project to be created. This may not necessarily be an error, for example if the user canceled out of the create flow.');
} else {
// else an error would occur during the createFunction
exitReason = ExitReason.error;
void vscode.window.showErrorMessage(constants.errorNewAzureFunction(error));
}
// an error occurred during createFunction
exitReason = ExitReason.error;
void vscode.window.showErrorMessage(constants.errorNewAzureFunction(error));
TelemetryReporter.createErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick, undefined, errorType)
.withAdditionalProperties(propertyBag).send();
return;
@@ -278,7 +283,6 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
propertyBag.exitReason = exitReason;
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick)
.withAdditionalProperties(propertyBag).send();
newFunctionFileObject?.watcherDisposable.dispose();
}
}

View File

@@ -7,7 +7,6 @@ import * as fs from 'fs';
import * as path from 'path';
import * as should from 'should';
import * as sinon from 'sinon';
import * as TypeMoq from 'typemoq';
import * as vscode from 'vscode';
import * as azureFunctionUtils from '../../common/azureFunctionsUtils';
import * as constants from '../../common/constants';
@@ -90,9 +89,6 @@ describe('AzureFunctionsService', () => {
sinon.stub(azureFunctionUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'SqlConnectionString', 'testConnectionString').resolves((true));
sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package');
const testWatcher = TypeMoq.Mock.ofType<vscode.FileSystemWatcher>().object;
sinon.stub(azureFunctionUtils, 'waitForNewFunctionFile').withArgs(sinon.match.any).returns({ filePromise: Promise.resolve('TestFileCreated'), watcherDisposable: testWatcher });
should(connectionInfo.database).equal('my_db', 'Initial ConnectionInfo database should be my_db');
await azureFunctionService.createAzureFunction(tableTestNode);
@@ -127,9 +123,6 @@ describe('AzureFunctionsService', () => {
sinon.stub(azureFunctionUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'SqlConnectionString', 'testConnectionString').resolves((true));
sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package');
const testWatcher = TypeMoq.Mock.ofType<vscode.FileSystemWatcher>().object;
sinon.stub(azureFunctionUtils, 'waitForNewFunctionFile').withArgs(sinon.match.any).returns({ filePromise: Promise.resolve('TestFileCreated'), watcherDisposable: testWatcher });
showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); // error message spy to be used for checking tests
});
@@ -221,9 +214,6 @@ describe('AzureFunctionsService', () => {
sinon.stub(azureFunctionUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'SqlConnectionString', 'testConnectionString').resolves((true));
sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package');
const testWatcher = TypeMoq.Mock.ofType<vscode.FileSystemWatcher>().object;
sinon.stub(azureFunctionUtils, 'waitForNewFunctionFile').withArgs(sinon.match.any).returns({ filePromise: Promise.resolve('TestFileCreated'), watcherDisposable: testWatcher });
showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); // error message spy to be used for checking tests
});

View File

@@ -2,7 +2,7 @@
"name": "sql-database-projects",
"displayName": "SQL Database Projects",
"description": "Enables users to develop and publish database schemas for MSSQL Databases",
"version": "0.18.0",
"version": "0.19.0",
"publisher": "Microsoft",
"preview": true,
"engines": {
@@ -59,6 +59,10 @@
"sqlDatabaseProjects.autorestSqlVersion": {
"type": "string",
"description": "%sqlDatabaseProjects.autorestSqlVersion%"
},
"sqlDatabaseProjects.collapseProjectNodes": {
"type": "boolean",
"description": "%sqlDatabaseProjects.collapseProjectNodes%"
}
}
}

View File

@@ -40,5 +40,6 @@
"sqlDatabaseProjects.netCoreDoNotAsk": "Whether to prompt the user to install .NET Core when not detected.",
"sqlDatabaseProjects.nodejsDoNotAsk": "Whether to prompt the user to install Node.js when not detected.",
"sqlDatabaseProjects.autorestSqlVersion": "Which version of Autorest.Sql to use from NPM. Latest will be used if not set.",
"sqlDatabaseProjects.collapseProjectNodes": "Whether project nodes start collapsed",
"sqlDatabaseProjects.welcome": "No database projects currently open.\n[New Project](command:sqlDatabaseProjects.new)\n[Open Project](command:sqlDatabaseProjects.open)\n[Create Project From Database](command:sqlDatabaseProjects.importDatabase)"
}

View File

@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import * as path from 'path';
import { SqlTargetPlatform } from 'sqldbproj';
import * as utils from '../common/utils';
@@ -150,16 +149,18 @@ export const nameMustNotBeEmpty = localize('nameMustNotBeEmpty', "Name must not
export const AdvancedOptionsButton = localize('advancedOptionsButton', 'Advanced...');
export const AdvancedPublishOptions = localize('advancedPublishOptions', 'Advanced Publish Options');
export const PublishOptions = localize('publishOptions', 'Publish Options');
export const ExcludeObjectTypeTab = localize('excludeObjectTypes', 'Exclude Object Types');
export const ResetButton: string = localize('reset', "Reset");
export const OptionDescription: string = localize('optionDescription', "Option Description");
export const OptionName: string = localize('optionName', "Option Name");
export const OptionInclude: string = localize('Include', "Include");
export function OptionNotFoundWarningMessage(label: string) { return localize('OptionNotFoundWarningMessage', "label: {0} does not exist in the options value name lookup", label); }
export const OptionInclude: string = localize('include', "Include");
export function OptionNotFoundWarningMessage(label: string) { return localize('optionNotFoundWarningMessage', "label: {0} does not exist in the options value name lookup", label); }
// Deploy
export const SqlServerName = 'SQL server';
export const AzureSqlServerName = 'Azure SQL server';
export const SqlServerDockerImageName = 'Microsoft SQL Server';
export const SqlServerDocker2022ImageName = 'Microsoft SQL Server 2022 (preview)';
export const AzureSqlDbFullDockerImageName = 'Azure SQL Database emulator Full';
export const AzureSqlDbLiteDockerImageName = 'Azure SQL Database emulator Lite';
export const AzureSqlLogicalServerName = 'Azure SQL logical server';
@@ -201,8 +202,8 @@ export const eulaAgreementTemplate = localize({ key: 'eulaAgreementTemplate', co
export function eulaAgreementText(name: string) { return localize({ key: 'eulaAgreementText', comment: ['The placeholders are contents of the line and should not be translated.'] }, "I accept the {0}.", name); }
export const eulaAgreementTitle = localize('eulaAgreementTitle', "Microsoft SQL Server License Agreement");
export const edgeEulaAgreementTitle = localize('edgeEulaAgreementTitle', "Microsoft Azure SQL Edge License Agreement");
export const sqlServerEulaLink = 'https://go.microsoft.com/fwlink/?linkid=857698';
export const sqlServerEdgeEulaLink = 'https://go.microsoft.com/fwlink/?linkid=2139274';
export const sqlServerEulaLink = 'https://aka.ms/mcr/osslegalnotice';
export const sqlServerEdgeEulaLink = 'https://aka.ms/mcr/osslegalnotice';
export const connectionNamePrefix = 'SQLDbProject';
export const sqlServerDockerRegistry = 'mcr.microsoft.com';
export const sqlServerDockerRepository = 'mssql/server';
@@ -456,7 +457,6 @@ export const Sdk: string = 'Sdk';
export const DatabaseSource = 'DatabaseSource';
export const VisualStudioVersion = 'VisualStudioVersion';
export const SSDTExists = 'SSDTExists';
export const OutputPath = 'OutputPath';
export const BuildElements = localize('buildElements', "Build Elements");
export const FolderElements = localize('folderElements', "Folder Elements");
@@ -495,8 +495,6 @@ export const RoundTripSqlDbNotPresentCondition = '\'$(NetCoreBuild)\' != \'true\
export const DacpacRootPath = '$(DacPacRootPath)';
export const ProjJsonToClean = '$(BaseIntermediateOutputPath)\\project.assets.json';
export function defaultOutputPath() { return path.join('bin', 'Debug'); }
// Sqlproj VS property conditions
export const VSVersionCondition = '\'$(VisualStudioVersion)\' == \'\'';
export const SsdtExistsCondition = '\'$(SSDTExists)\' == \'\'';
@@ -605,3 +603,5 @@ export enum PublishTargetType {
docker = 'docker',
newAzureServer = 'newAzureServer'
}
export const CollapseProjectNodesKey = 'collapseProjectNodes';

View File

@@ -325,13 +325,6 @@ export async function defaultAzureAccountServiceFactory(): Promise<vscodeMssql.I
export async function getDefaultPublishDeploymentOptions(project: ISqlProject): Promise<mssql.DeploymentOptions | vscodeMssql.DeploymentOptions> {
const schemaCompareService = await getSchemaCompareService();
const result = await schemaCompareService.schemaCompareGetDefaultOptions();
// re-include database-scoped credentials
if (getAzdataApi()) {
result.defaultDeploymentOptions.excludeObjectTypes.value = (result.defaultDeploymentOptions as mssql.DeploymentOptions).excludeObjectTypes.value?.filter(x => x !== mssql.SchemaObjectType.DatabaseScopedCredentials);
} else {
result.defaultDeploymentOptions.excludeObjectTypes.value = (result.defaultDeploymentOptions as vscodeMssql.DeploymentOptions).excludeObjectTypes.value?.filter(x => x !== vscodeMssql.SchemaObjectType.DatabaseScopedCredentials);
}
// this option needs to be true for same database references validation to work
if (project.databaseReferences.length > 0) {
result.defaultDeploymentOptions.booleanOptionsDictionary.includeCompositeObjects.value = true;

View File

@@ -13,11 +13,11 @@ import { SqlConnectionDataSource } from '../models/dataSources/sqlConnectionStri
import { DeploymentOptions } from 'mssql';
import { IconPathHelper } from '../common/iconHelper';
import { cssStyles } from '../common/uiConstants';
import { getAgreementDisplayText, getConnectionName, getDockerBaseImages, getPublishServerName } from './utils';
import { getAgreementDisplayText, getConnectionName, getDefaultDockerImageWithTag, getDockerBaseImages, getPublishServerName } from './utils';
import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
import { Deferred } from '../common/promise';
import { PublishOptionsDialog } from './publishOptionsDialog';
import { ISqlProjectPublishSettings, IPublishToDockerSettings } from 'sqldbproj';
import { ISqlProjectPublishSettings, IPublishToDockerSettings, SqlTargetPlatform } from 'sqldbproj';
interface DataSourceDropdownValue extends azdataType.CategoryValue {
dataSource: SqlConnectionDataSource;
@@ -145,7 +145,6 @@ export class PublishDatabaseDialog {
this.connectionRow = this.createConnectionRow(view);
this.databaseRow = this.createDatabaseRow(view);
const displayOptionsButton = this.createOptionsButton(view);
displayOptionsButton.enabled = false;
const horizontalFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
horizontalFormSection.addItems([profileRow, this.databaseRow]);
@@ -172,12 +171,10 @@ export class PublishDatabaseDialog {
title: constants.selectConnectionRadioButtonsTitle,
component: selectConnectionRadioButtons
},*/
/* TODO : Disabling deployment options for the July release
{
component: displayOptionsButton,
title: ''
}
*/
]
}
], {
@@ -244,9 +241,14 @@ export class PublishDatabaseDialog {
utils.getAzdataApi()!.window.closeDialog(this.dialog);
await this.publish!(this.project, settings);
} else {
const dockerBaseImage = this.getBaseDockerImageName();
let dockerBaseImage = this.getBaseDockerImageName();
const baseImages = getDockerBaseImages(this.project.getProjectTargetVersion());
const imageInfo = baseImages.find(x => x.name === dockerBaseImage);
// selecting the image tag isn't currently exposed in the publish dialog, so this adds the tag matching the target platform
// to make sure the correct image is used for the project's target platform when the docker base image is SQL Server
dockerBaseImage = getDefaultDockerImageWithTag(this.project.getProjectTargetVersion(), dockerBaseImage, imageInfo);
const settings: IPublishToDockerSettings = {
dockerSettings: {
dbName: this.targetDatabaseName,
@@ -601,6 +603,16 @@ export class PublishDatabaseDialog {
const baseImages = getDockerBaseImages(this.project.getProjectTargetVersion());
const baseImagesValues: azdataType.CategoryValue[] = baseImages.map(x => { return { name: x.name, displayName: x.displayName }; });
// add preview string for 2022
// TODO: remove after 2022 is GA
if (this.project.getProjectTargetVersion() === constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2022)) {
const sqlServerImageIndex = baseImagesValues.findIndex(image => image.displayName === constants.SqlServerDockerImageName);
if (sqlServerImageIndex >= 0) {
baseImagesValues[sqlServerImageIndex].displayName = constants.SqlServerDocker2022ImageName;
}
}
this.baseDockerImageDropDown = view.modelBuilder.dropDown().withProps({
values: baseImagesValues,
ariaLabel: constants.baseDockerImage(name),
@@ -836,6 +848,9 @@ export class PublishDatabaseDialog {
this.targetDatabaseName = result.databaseName;
}
// set options coming from the publish profiles to deployment options
this.setDeploymentOptions(result.options);
if (Object.keys(result.sqlCmdVariables).length) {
// add SQLCMD Variables table if it wasn't there before and the profile had sqlcmd variables
if (Object.keys(this.project.sqlCmdVariables).length === 0 && Object.keys(<Record<string, string>>this.sqlCmdVars).length === 0) {
@@ -935,7 +950,12 @@ export class PublishDatabaseDialog {
* Gets the default deployment options from the dacfx service
*/
public async getDefaultDeploymentOptions(): Promise<DeploymentOptions> {
return await utils.getDefaultPublishDeploymentOptions(this.project) as DeploymentOptions;
const defaultDeploymentOptions = await utils.getDefaultPublishDeploymentOptions(this.project) as DeploymentOptions;
if (defaultDeploymentOptions && defaultDeploymentOptions.excludeObjectTypes !== undefined) {
// For publish dialog no default exclude options should exists
defaultDeploymentOptions.excludeObjectTypes.value = [];
}
return defaultDeploymentOptions;
}
/*

View File

@@ -24,6 +24,9 @@ export class PublishOptionsDialog {
private optionsFlexBuilder: azdataType.FlexContainer | undefined;
private optionsChanged: boolean = false;
private isResetOptionsClicked: boolean = false;
private excludeObjectTypesOptionsTab: azdataType.window.DialogTab | undefined;
private excludeObjectTypesOptionsTable: azdataType.TableComponent | undefined;
private excludeObjectTypesOptionsFlexBuilder: azdataType.FlexContainer | undefined;
constructor(defaultOptions: mssql.DeploymentOptions, private publish: PublishDatabaseDialog) {
this.optionsModel = new DeployOptionsModel(defaultOptions);
@@ -31,8 +34,10 @@ export class PublishOptionsDialog {
protected initializeDialog(): void {
this.optionsTab = utils.getAzdataApi()!.window.createTab(constants.PublishOptions);
this.intializeDeploymentOptionsDialogTab();
this.dialog.content = [this.optionsTab];
this.excludeObjectTypesOptionsTab = utils.getAzdataApi()!.window.createTab(constants.ExcludeObjectTypeTab);
this.initializeDeploymentOptionsDialogTab();
this.initializeExcludeObjectTypesOptionsDialogTab();
this.dialog.content = [this.optionsTab, this.excludeObjectTypesOptionsTab];
}
public openDialog(): void {
@@ -55,8 +60,26 @@ export class PublishOptionsDialog {
utils.getAzdataApi()!.window.openDialog(this.dialog);
}
private intializeDeploymentOptionsDialogTab(): void {
private initializeDeploymentOptionsDialogTab(): void {
this.optionsTab?.registerContent(async view => {
// create loading component
const loader = view.modelBuilder.loadingComponent()
.withProps({
CSSStyles: {
'margin-top': '50%'
}
})
.component();
this.optionsFlexBuilder = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column'
}).component();
// adding loading component to the flexcontainer
this.optionsFlexBuilder.addItem(loader);
await view.initializeModel(this.optionsFlexBuilder);
this.descriptionHeading = view.modelBuilder.table().withProps({
data: [],
columns: [
@@ -99,17 +122,41 @@ export class PublishOptionsDialog {
}
}));
this.optionsFlexBuilder = view.modelBuilder.flexContainer()
this.optionsFlexBuilder.addItem(this.optionsTable, { CSSStyles: { 'overflow': 'scroll', 'height': '65vh', 'padding-top': '2px' } });
this.optionsFlexBuilder.addItem(this.descriptionHeading, { CSSStyles: { 'font-weight': 'bold', 'height': '30px' } });
this.optionsFlexBuilder.addItem(this.descriptionText, { CSSStyles: { 'padding': '4px', 'margin-right': '10px', 'overflow': 'scroll', 'height': '10vh' } });
loader.loading = false;
await view.initializeModel(this.optionsFlexBuilder);
// focus the first option
await this.optionsTable.focus();
});
}
private initializeExcludeObjectTypesOptionsDialogTab(): void {
this.excludeObjectTypesOptionsTab?.registerContent(async view => {
this.excludeObjectTypesOptionsTable = view.modelBuilder.table().component();
await this.updateExcludeObjectsTable();
// Update exclude type options value on checkbox onchange
this.disposableListeners.push(this.excludeObjectTypesOptionsTable!.onCellAction!((rowState) => {
const checkboxState = <azdataType.ICheckboxCellActionEventArgs>rowState;
if (checkboxState && checkboxState.row !== undefined) {
// data[row][1] contains the exclude type option display name
const displayName = this.excludeObjectTypesOptionsTable?.data[checkboxState.row][1];
this.optionsModel.setExcludeObjectTypesOptionValue(displayName, checkboxState.checked);
this.optionsChanged = true;
// customButton[0] is the reset button, enabling it when option checkbox is changed
this.dialog.customButtons[0].enabled = true;
}
}));
this.excludeObjectTypesOptionsFlexBuilder = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column'
}).component();
this.optionsFlexBuilder.addItem(this.optionsTable, { CSSStyles: { 'overflow': 'scroll', 'height': '65vh', 'padding-top': '2px' } });
this.optionsFlexBuilder.addItem(this.descriptionHeading, { CSSStyles: { 'font-weight': 'bold', 'height': '30px' } });
this.optionsFlexBuilder.addItem(this.descriptionText, { CSSStyles: { 'padding': '4px', 'margin-right': '10px', 'overflow': 'scroll', 'height': '10vh' } });
await view.initializeModel(this.optionsFlexBuilder);
// focus the first option
await this.optionsTable.focus();
this.excludeObjectTypesOptionsFlexBuilder.addItem(this.excludeObjectTypesOptionsTable, { CSSStyles: { 'overflow': 'scroll', 'height': '80vh', 'padding-top': '2px' } });
await view.initializeModel(this.excludeObjectTypesOptionsFlexBuilder);
});
}
@@ -141,12 +188,41 @@ export class PublishOptionsDialog {
});
}
/*
* Update the default options to the exclude objects table area
*/
private async updateExcludeObjectsTable(): Promise<void> {
const data = this.optionsModel.getExcludeObjectTypesOptionsData();
await this.excludeObjectTypesOptionsTable?.updateProperties({
data: data,
columns: [
<azdataType.CheckboxColumn>
{
value: constants.OptionInclude,
type: utils.getAzdataApi()!.ColumnType.checkBox,
action: utils.getAzdataApi()!.ActionOnCellCheckboxCheck.customAction,
headerCssClass: 'display-none',
cssClass: 'no-borders align-with-header',
width: 50
},
{
value: constants.OptionName,
headerCssClass: 'display-none',
cssClass: 'no-borders align-with-header',
width: 50
}
],
ariaRowCount: data.length
});
}
/*
* Ok button click, will update the deployment options with selections
*/
protected execute(): void {
// Update the model deploymentoptions with the updated table component values
// Update the model deploymentoptions with the updated options/excludeObjects table component values
this.optionsModel.setDeploymentOptions();
this.optionsModel.setExcludeObjectTypesToDeploymentOptions();
// Set the publish deploymentoptions with the updated table component values
this.publish.setDeploymentOptions(this.optionsModel.deploymentOptions);
this.disposeListeners();
@@ -173,14 +249,19 @@ export class PublishOptionsDialog {
const result = await this.publish.getDefaultDeploymentOptions();
this.optionsModel.deploymentOptions = result;
// reset optionsvalueNameLookup with default deployment options
// reset optionsvalueNameLookup and excludeObjectTypesLookup with default deployment options
this.optionsModel.setOptionsToValueNameLookup();
this.optionsModel.setExcludeObjectTypesLookup();
await this.updateOptionsTable();
this.optionsFlexBuilder?.removeItem(this.optionsTable!);
this.optionsFlexBuilder?.insertItem(this.optionsTable!, 0, { CSSStyles: { 'overflow': 'scroll', 'height': '65vh', 'padding-top': '2px' } });
TelemetryReporter.sendActionEvent(TelemetryViews.PublishOptionsDialog, TelemetryActions.resetOptions);
await this.updateExcludeObjectsTable();
this.excludeObjectTypesOptionsFlexBuilder?.removeItem(this.excludeObjectTypesOptionsTable!);
this.excludeObjectTypesOptionsFlexBuilder?.addItem(this.excludeObjectTypesOptionsTable!, { CSSStyles: { 'overflow': 'scroll', 'height': '80vh', 'padding-top': '2px' } });
// setting optionsChanged to false when reset click, if optionsChanged is true during execute, that means there is an option changed after reset
this.isResetOptionsClicked = true;
this.optionsChanged = false;

View File

@@ -155,3 +155,32 @@ export function getDockerBaseImages(target: string): DockerImageInfo[] {
];
}
}
/**
* This adds the tag matching the target platform to make sure the correct image is used for the project's target platform when the docker base image is SQL Server.
* If the image is Edge, then no tag is appended
* @param projectTargetVersion target version of the project
* @param dockerImage selected base docker image without tag
* @param imageInfo docker image info of the selected docker image
* @returns dockerBaseImage with the appropriate image tag appended if there is one
*/
export function getDefaultDockerImageWithTag(projectTargetVersion: string, dockerImage: string, imageInfo?: DockerImageInfo,): string {
if (imageInfo?.displayName === constants.SqlServerDockerImageName) {
switch (projectTargetVersion) {
case constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2022):
dockerImage = `${dockerImage}:2022-latest`;
break;
case constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2019):
dockerImage = `${dockerImage}:2019-latest`;
break;
case constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2017):
dockerImage = `${dockerImage}:2017-latest`;
break;
default:
// nothing - let it be the default image defined as default in the container registry
break;
}
}
return dockerImage;
}

View File

@@ -10,9 +10,11 @@ import * as constants from '../../common/constants';
export class DeployOptionsModel {
// key is the option display name and values are checkboxValue and optionName
private optionsValueNameLookup: { [key: string]: mssql.IOptionWithValue } = {};
private excludeObjectTypesLookup: { [key: string]: mssql.IOptionWithValue } = {};
constructor(public deploymentOptions: mssql.DeploymentOptions) {
this.setOptionsToValueNameLookup();
this.setExcludeObjectTypesLookup();
}
/*
@@ -69,4 +71,62 @@ export class DeployOptionsModel {
}
return optionName !== undefined ? this.deploymentOptions.booleanOptionsDictionary[optionName.optionName].description : '';
}
/*
* Sets exclude object types option's checkbox values and property name to the excludeObjectTypesLookup map
*/
public setExcludeObjectTypesLookup(): void {
Object.entries(this.deploymentOptions.objectTypesDictionary).forEach(option => {
const optionValue: mssql.IOptionWithValue = {
optionName: option[0],
checked: this.getExcludeObjectTypeOptionCheckStatus(option[0])
};
this.excludeObjectTypesLookup[option[1]] = optionValue;
});
}
/*
* Initialize options data from objectTypesDictionary for table component
* Returns data as [booleanValue, optionName]
*/
public getExcludeObjectTypesOptionsData(): any[][] {
let data: any[][] = [];
Object.entries(this.deploymentOptions.objectTypesDictionary).forEach(option => {
// option[1] is the display name and option[0] is the optionName
data.push([this.getExcludeObjectTypeOptionCheckStatus(option[0]), option[1]]);
});
return data.sort((a, b) => a[1].localeCompare(b[1]));
}
/*
* Gets the selected/default value of the object type option
* return true for the deploymentOptions.excludeObjectTypes option, if it is in ObjectTypesDictionary
*/
public getExcludeObjectTypeOptionCheckStatus(optionName: string): boolean {
return (this.deploymentOptions.excludeObjectTypes.value?.find(x => x.toLowerCase() === optionName.toLowerCase())) !== undefined ? true : false;
}
/*
* Sets the checkbox value to the excludeObjectTypesLookup map
*/
public setExcludeObjectTypesOptionValue(displayName: string, checked: boolean): void {
this.excludeObjectTypesLookup[displayName].checked = checked;
}
/*
* Sets the selected option checkbox value to the deployment options
*/
public setExcludeObjectTypesToDeploymentOptions(): void {
let finalExcludedObjectTypes: string[] = [];
Object.entries(this.excludeObjectTypesLookup).forEach(option => {
// option[1] holds checkedbox value and optionName
if (option[1].checked) {
finalExcludedObjectTypes.push(option[1].optionName);
}
});
this.deploymentOptions.excludeObjectTypes.value = finalExcludedObjectTypes;
}
}

View File

@@ -36,10 +36,9 @@ export class Project implements ISqlProject {
private _postDeployScripts: FileProjectEntry[] = [];
private _noneDeployScripts: FileProjectEntry[] = [];
private _isSdkStyleProject: boolean = false; // https://docs.microsoft.com/en-us/dotnet/core/project-sdk/overview
private _outputPath: string = '';
public get dacpacOutputPath(): string {
return path.join(this.outputPath, `${this._projectFileName}.dacpac`);
return path.join(this.projectFolderPath, 'bin', 'Debug', `${this._projectFileName}.dacpac`);
}
public get projectFolderPath() {
@@ -94,10 +93,6 @@ export class Project implements ISqlProject {
return this._isSdkStyleProject;
}
public get outputPath(): string {
return this._outputPath;
}
private projFileXmlDoc: Document | undefined = undefined;
constructor(projectFilePath: string) {
@@ -160,16 +155,6 @@ export class Project implements ISqlProject {
this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.PropertyGroup)[0]?.appendChild(newProjectGuidNode);
await this.serializeToProjFile(this.projFileXmlDoc);
}
// get output path
const outputNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.OutputPath);
if (outputNodes.length > 0) {
const outputPath = outputNodes[0].childNodes[0].nodeValue!;
this._outputPath = path.join(utils.getPlatformSafeFileEntryPath(this.projectFolderPath), utils.getPlatformSafeFileEntryPath(outputPath));
} else {
// If output path isn't specified in .sqlproj, set it to the default output path .\bin\Debug\
this._outputPath = path.join(utils.getPlatformSafeFileEntryPath(this.projectFolderPath), utils.getPlatformSafeFileEntryPath(constants.defaultOutputPath()));
}
}
/**

View File

@@ -11,10 +11,11 @@ import * as fileTree from './fileFolderTreeItem';
import { Project } from '../project';
import * as utils from '../../common/utils';
import { DatabaseReferencesTreeItem } from './databaseReferencesTreeItem';
import { DatabaseProjectItemType, RelativeOuterPath, ExternalStreamingJob, sqlprojExtension } from '../../common/constants';
import { DatabaseProjectItemType, RelativeOuterPath, ExternalStreamingJob, sqlprojExtension, CollapseProjectNodesKey } from '../../common/constants';
import { IconPathHelper } from '../../common/iconHelper';
import { FileProjectEntry } from '../projectEntry';
import { EntryType } from 'sqldbproj';
import { DBProjectConfigurationKey } from '../../tools/netcoreTool';
/**
* TreeNode root that represents an entire project
@@ -47,7 +48,8 @@ export class ProjectRootTreeItem extends BaseProjectTreeItem {
}
public get treeItem(): vscode.TreeItem {
const projectItem = new vscode.TreeItem(this.fileSystemUri, vscode.TreeItemCollapsibleState.Expanded);
const collapsibleState = vscode.workspace.getConfiguration(DBProjectConfigurationKey)[CollapseProjectNodesKey] ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.Expanded;
const projectItem = new vscode.TreeItem(this.fileSystemUri, collapsibleState);
projectItem.contextValue = this.project.isSdkStyleProject ? DatabaseProjectItemType.project : DatabaseProjectItemType.legacyProject;
projectItem.iconPath = IconPathHelper.databaseProject;
projectItem.label = path.basename(this.projectUri.fsPath, sqlprojExtension);

View File

@@ -6,7 +6,6 @@
<ProjectGuid>{2C283C5D-9E4A-4313-8FF9-4E0CEE20B063}</ProjectGuid>
<DSP>Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider</DSP>
<ModelCollation>1033, CI</ModelCollation>
<OutputPath>..\otherFolder</OutputPath>
</PropertyGroup>
<Target Name="BeforeBuild">
<Delete Files="$(BaseIntermediateOutputPath)\project.assets.json" />

View File

@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import * as constants from '../../common/constants';
import { SqlTargetPlatform } from 'sqldbproj';
import { getDefaultDockerImageWithTag, getDockerBaseImages } from '../../dialogs/utils';
describe('Tests to verify dialog utils functions', function (): void {
it('getDefaultDockerImageWithTag should return correct image', () => {
const baseImages = getDockerBaseImages(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2022)!);
const sqlServerImageInfo = baseImages.find(image => image.displayName === constants.SqlServerDockerImageName);
const edgeImageInfo = baseImages.find(image => image.displayName === SqlTargetPlatform.sqlEdge);
should(getDefaultDockerImageWithTag('160', 'mcr.microsoft.com/mssql/server', sqlServerImageInfo)).equals(`${sqlServerImageInfo?.name}:2022-latest`, 'Unexpected docker image returned for target platform SQL Server 2022 and SQL Server base image');
should(getDefaultDockerImageWithTag('150', 'mcr.microsoft.com/mssql/server', sqlServerImageInfo)).equals(`${sqlServerImageInfo?.name}:2019-latest`, 'Unexpected docker image returned for target platform SQL Server 2019 and SQL Server base image');
should(getDefaultDockerImageWithTag('140', 'mcr.microsoft.com/mssql/server', sqlServerImageInfo)).equals(`${sqlServerImageInfo?.name}:2017-latest`, 'Unexpected docker image returned for target platform SQL Server 2017 and SQL Server base image');
should(getDefaultDockerImageWithTag('130', 'mcr.microsoft.com/mssql/server', sqlServerImageInfo)).equals(`${sqlServerImageInfo?.name}`, 'Unexpected docker image returned for target platform SQL Server 2016 and SQL Server base image');
should(getDefaultDockerImageWithTag('150', 'mcr.microsoft.com/azure-sql-edge', edgeImageInfo)).equals(`${edgeImageInfo?.name}`, 'Unexpected docker image returned for target platform SQL Server 2019 and Edge base image');
// different display names are returned when a project's target platform is Azure, but currently the Azure full image points to mcr.microsoft.com/mssql/server
const azureBaseImages = getDockerBaseImages(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlAzure)!);
const azureFullImageInfo = azureBaseImages.find(image => image.displayName === constants.AzureSqlDbFullDockerImageName);
const azureLiteImageInfo = azureBaseImages.find(image => image.displayName === constants.AzureSqlDbLiteDockerImageName);
should(getDefaultDockerImageWithTag('AzureV12', 'mcr.microsoft.com/mssql/server', azureFullImageInfo)).equals(`${azureFullImageInfo?.name}`, 'Unexpected docker image returned for target platform Azure and Azure full base image');
should(getDefaultDockerImageWithTag('AzureV12', 'mcr.microsoft.com/azure-sql-edge', azureLiteImageInfo)).equals(`${azureLiteImageInfo?.name}`, 'Unexpected docker image returned for target platform Azure Azure lite base image');
});
});

View File

@@ -19,7 +19,7 @@ describe('Publish Dialog Deploy Options Model', () => {
const model = new DeployOptionsModel(testUtils.getDeploymentOptions());
Object.entries(model.deploymentOptions.booleanOptionsDictionary).forEach(option => {
// option[1] contains the value, description and displayName
should(model.getOptionDescription(option[1].displayName)).not.equal(undefined);
should(model.getOptionDescription(option[1].displayName)).not.equal(undefined, 'publish option description should not be undefined');
});
});
@@ -27,4 +27,31 @@ describe('Publish Dialog Deploy Options Model', () => {
const model = new DeployOptionsModel(testUtils.getDeploymentOptions());
should(model.getOptionDescription('')).equal('');
});
it('Should have no default exclude object types', function (): void {
const model = new DeployOptionsModel(testUtils.getDeploymentOptions());
should(model.deploymentOptions.excludeObjectTypes.value.length).be.equal(0, 'There should be no object types excluded from excludeObjectTypes');
// should return true for all object type options as there are no default excludeObjectTypes in the deployment options
Object.keys(model.deploymentOptions.objectTypesDictionary).forEach(option => {
should(model.getExcludeObjectTypeOptionCheckStatus(option)).equal(false, 'excludeObjectTypes property should be empty by default and return false');
});
});
it('Should have atleast one default exclude object types', function (): void {
const model = new DeployOptionsModel(testUtils.getDeploymentOptions());
model.deploymentOptions.excludeObjectTypes.value = ['SampleProperty1'];
should(model.deploymentOptions.excludeObjectTypes.value.length).be.equal(1, 'There should be one excluded object');
// should return true for all exclude object types options and false for the exising defauit option
Object.keys(model.deploymentOptions.objectTypesDictionary).forEach(option => {
if (option === 'SampleProperty1') {
should(model.getExcludeObjectTypeOptionCheckStatus(option)).equal(true, 'should return true for the excludeObjectTypes SampleProperty1 ');
} else {
should(model.getExcludeObjectTypeOptionCheckStatus(option)).equal(false, 'should return false for all excludeObjectTypes property as it is empty');
}
});
});
});

View File

@@ -13,7 +13,7 @@ import * as constants from '../common/constants';
import { promises as fs } from 'fs';
import { Project } from '../models/project';
import { exists, convertSlashesForSqlProj, getWellKnownDatabaseSources, getPlatformSafeFileEntryPath } from '../common/utils';
import { exists, convertSlashesForSqlProj, getWellKnownDatabaseSources } from '../common/utils';
import { Uri, window } from 'vscode';
import { IDacpacReferenceSettings, IProjectReferenceSettings, ISystemDatabaseReferenceSettings } from '../models/IDatabaseReferenceSettings';
import { EntryType, ItemType, SqlTargetPlatform } from 'sqldbproj';
@@ -1430,30 +1430,6 @@ describe('Project: sdk style project content operations', function (): void {
should(projFileText.includes(constants.ProjectGuid)).equal(true);
});
it('Should read OutputPath from sqlproj if there is one', async function (): Promise<void> {
projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectBaseline);
const projFileText = (await fs.readFile(projFilePath)).toString();
// Verify sqlproj has OutputPath
should(projFileText.includes(constants.OutputPath)).equal(true);
const project: Project = await Project.openProject(projFilePath);
should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('..\\otherFolder')));
should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('..\\otherFolder'), `${project.projectFileName}.dacpac`));
});
it('Should use default output path if OutputPath is not specified in sqlproj', async function (): Promise<void> {
projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectWithGlobsSpecifiedBaseline);
const projFileText = (await fs.readFile(projFilePath)).toString();
// Verify sqlproj doesn't have OutputPath
should(projFileText.includes(constants.OutputPath)).equal(true);
const project: Project = await Project.openProject(projFilePath);
should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath(constants.defaultOutputPath())));
should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath(constants.defaultOutputPath()), `${project.projectFileName}.dacpac`));
});
it('Should handle adding existing items to project', async function (): Promise<void> {
projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectBaseline);
const projectFolder = path.dirname(projFilePath);

View File

@@ -31,6 +31,10 @@ export function getDeploymentOptions(): mssql.DeploymentOptions {
booleanOptionsDictionary: {
'SampleProperty1': { value: false, description: sampleDesc, displayName: sampleName },
'SampleProperty2': { value: false, description: sampleDesc, displayName: sampleName }
},
objectTypesDictionary: {
'SampleProperty1': sampleName,
'SampleProperty2': sampleName
}
};
return defaultOptions;

View File

@@ -0,0 +1,3 @@
<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.999 7.5V8.5H7.99902V15.5H6.99902V8.5H-0.000976562V7.5H6.99902V0.5H7.99902V7.5H14.999Z" fill="#0078D4"/>
</svg>

After

Width:  |  Height:  |  Size: 220 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 2H14V14H1V2ZM2 4H13V3H2V4ZM5 7V5H2V7H5ZM2 8V10H5V8H2ZM5 13V11H2V13H5ZM9 7V5H6V7H9ZM6 8V10H9V8H6ZM9 13V11H6V13H9ZM13 7V5H10V7H13ZM13 10V8H10V10H13ZM13 13V11H10V13H13Z" fill="#323130"/>
</svg>

After

Width:  |  Height:  |  Size: 299 B

View File

@@ -0,0 +1,3 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<defs><style>.cls-1{fill:#231f20;}</style></defs><title>chevron_right</title><polygon class="cls-1" points="13.59 2.21 13.58 2.22 13.58 2.2 13.59 2.21"/><path d="M10.54,8l-7-7,1-1,8,8-8,8-1-1Z"/>
</svg>

After

Width:  |  Height:  |  Size: 299 B

View File

@@ -2,7 +2,7 @@
"name": "sql-migration",
"displayName": "%displayName%",
"description": "%description%",
"version": "1.0.4",
"version": "1.0.5",
"publisher": "Microsoft",
"preview": false,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
@@ -147,32 +147,7 @@
"when": "connectionProvider == 'MSSQL' && !mssql:iscloud && mssql:engineedition != 8",
"hideRefreshTask": true,
"container": {
"grid-container": [
{
"name": "",
"row": 0,
"col": 0,
"widget": {
"tasks-widget": [
"sqlmigration.start",
"sqlmigration.newsupportrequest",
"sqlmigration.sendfeedback"
]
}
},
{
"name": "",
"row": 0,
"col": 1,
"rowspan": 2.5,
"colspan": 3.5,
"widget": {
"modelview": {
"id": "migration.dashboard"
}
}
}
]
"modelview-container": null
}
}
]

View File

@@ -526,6 +526,22 @@ export interface MigrationStatusDetails {
lastRestoredFilename: string;
pendingLogBackupsCount: number;
invalidFiles: string[];
listOfCopyProgressDetails: CopyProgressDetail[];
}
export interface CopyProgressDetail {
tableName: string;
status: 'PreparingForCopy' | 'Copying' | 'CopyFinished' | 'RebuildingIndexes' | 'Succeeded' | 'Failed' | 'Canceled',
parallelCopyType: string;
usedParallelCopies: number;
dataRead: number;
dataWritten: number;
rowsRead: number;
rowsCopied: number;
copyStart: string;
copyThroughput: number,
copyDuration: number;
errors: string[];
}
export interface SqlConnectionInfo {

View File

@@ -5,13 +5,16 @@
import { window, Account, accounts, CategoryValue, DropDownComponent, IconPath } from 'azdata';
import { IconPathHelper } from '../constants/iconPathHelper';
import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel';
import { MigrationStatus, ProvisioningState } from '../models/migrationLocalStorage';
import * as crypto from 'crypto';
import * as azure from './azure';
import { azureResource, Tenant } from 'azurecore';
import * as constants from '../constants/strings';
import { logError, TelemetryViews } from '../telemtery';
import { AdsMigrationStatus } from '../dashboard/tabBase';
import { getMigrationMode, getMigrationStatus, getMigrationTargetType, PipelineStatusCodes } from '../constants/helper';
export const DefaultSettingValue = '---';
export function deepClone<T>(obj: T): T {
if (!obj || typeof obj !== 'object') {
@@ -92,34 +95,68 @@ export function convertTimeDifferenceToDuration(startTime: Date, endTime: Date):
}
}
export function filterMigrations(databaseMigrations: azure.DatabaseMigration[], statusFilter: string, databaseNameFilter?: string): azure.DatabaseMigration[] {
let filteredMigration: azure.DatabaseMigration[] = [];
if (statusFilter === AdsMigrationStatus.ALL) {
filteredMigration = databaseMigrations;
} else if (statusFilter === AdsMigrationStatus.ONGOING) {
filteredMigration = databaseMigrations.filter(
value => {
const status = value.properties?.migrationStatus;
return status === MigrationStatus.InProgress
|| status === MigrationStatus.Creating
|| value.properties?.provisioningState === MigrationStatus.Creating;
});
} else if (statusFilter === AdsMigrationStatus.SUCCEEDED) {
filteredMigration = databaseMigrations.filter(
value => value.properties?.migrationStatus === MigrationStatus.Succeeded);
} else if (statusFilter === AdsMigrationStatus.FAILED) {
filteredMigration = databaseMigrations.filter(
value =>
value.properties?.migrationStatus === MigrationStatus.Failed ||
value.properties?.provisioningState === ProvisioningState.Failed);
} else if (statusFilter === AdsMigrationStatus.COMPLETING) {
filteredMigration = databaseMigrations.filter(
value => value.properties?.migrationStatus === MigrationStatus.Completing);
export function getMigrationTime(migrationTime: string): string {
return migrationTime
? new Date(migrationTime).toLocaleString()
: DefaultSettingValue;
}
export function getMigrationDuration(startDate: string, endDate: string): string {
if (startDate) {
if (endDate) {
return convertTimeDifferenceToDuration(
new Date(startDate),
new Date(endDate));
} else {
return convertTimeDifferenceToDuration(
new Date(startDate),
new Date());
}
}
if (databaseNameFilter) {
const filter = databaseNameFilter.toLowerCase();
return DefaultSettingValue;
}
export function filterMigrations(databaseMigrations: azure.DatabaseMigration[], statusFilter: string, columnTextFilter?: string): azure.DatabaseMigration[] {
let filteredMigration: azure.DatabaseMigration[] = databaseMigrations || [];
if (columnTextFilter) {
const filter = columnTextFilter.toLowerCase();
filteredMigration = filteredMigration.filter(
migration => migration.name?.toLowerCase().includes(filter));
migration => migration.properties.sourceServerName?.toLowerCase().includes(filter)
|| migration.properties.sourceDatabaseName?.toLowerCase().includes(filter)
|| getMigrationStatus(migration)?.toLowerCase().includes(filter)
|| getMigrationMode(migration)?.toLowerCase().includes(filter)
|| getMigrationTargetType(migration)?.toLowerCase().includes(filter)
|| azure.getResourceName(migration.properties.scope)?.toLowerCase().includes(filter)
|| azure.getResourceName(migration.id)?.toLowerCase().includes(filter)
|| getMigrationDuration(
migration.properties.startedOn,
migration.properties.endedOn)?.toLowerCase().includes(filter)
|| getMigrationTime(migration.properties.startedOn)?.toLowerCase().includes(filter)
|| getMigrationTime(migration.properties.endedOn)?.toLowerCase().includes(filter)
|| getMigrationMode(migration)?.toLowerCase().includes(filter));
}
switch (statusFilter) {
case AdsMigrationStatus.ALL:
return filteredMigration;
case AdsMigrationStatus.ONGOING:
return filteredMigration.filter(
value => {
const status = getMigrationStatus(value);
return status === MigrationStatus.InProgress
|| status === MigrationStatus.Retriable
|| status === MigrationStatus.Creating;
});
case AdsMigrationStatus.SUCCEEDED:
return filteredMigration.filter(
value => getMigrationStatus(value) === MigrationStatus.Succeeded);
case AdsMigrationStatus.FAILED:
return filteredMigration.filter(
value => getMigrationStatus(value) === MigrationStatus.Failed);
case AdsMigrationStatus.COMPLETING:
return filteredMigration.filter(
value => getMigrationStatus(value) === MigrationStatus.Completing);
}
return filteredMigration;
}
@@ -208,12 +245,61 @@ export function decorate(decorator: (fn: Function, key: string) => Function): Fu
}
export function getSessionIdHeader(sessionId: string): { [key: string]: string } {
return {
'SqlMigrationSessionId': sessionId
};
return { 'SqlMigrationSessionId': sessionId };
}
export function getMigrationStatusImage(status: string): IconPath {
export function getMigrationStatusWithErrors(migration: azure.DatabaseMigration): string {
const properties = migration.properties;
const migrationStatus = properties.migrationStatus ?? properties.provisioningState;
let warningCount = 0;
if (properties.migrationFailureError?.message) {
warningCount++;
}
if (properties.migrationStatusDetails?.fileUploadBlockingErrors) {
const blockingErrors = properties.migrationStatusDetails?.fileUploadBlockingErrors.length ?? 0;
warningCount += blockingErrors;
}
if (properties.migrationStatusDetails?.restoreBlockingReason) {
warningCount++;
}
return constants.STATUS_VALUE(migrationStatus, warningCount)
+ (constants.STATUS_WARNING_COUNT(migrationStatus, warningCount) ?? '');
}
export function getPipelineStatusImage(status: string | undefined): IconPath {
// status codes: 'PreparingForCopy' | 'Copying' | 'CopyFinished' | 'RebuildingIndexes' | 'Succeeded' | 'Failed' | 'Canceled',
switch (status) {
case PipelineStatusCodes.Copying: // Copying: 'Copying',
return IconPathHelper.copy;
case PipelineStatusCodes.CopyFinished: // CopyFinished: 'CopyFinished',
case PipelineStatusCodes.RebuildingIndexes: // RebuildingIndexes: 'RebuildingIndexes',
return IconPathHelper.inProgressMigration;
case PipelineStatusCodes.Canceled: // Canceled: 'Canceled',
return IconPathHelper.cancel;
case PipelineStatusCodes.PreparingForCopy: // PreparingForCopy: 'PreparingForCopy',
return IconPathHelper.notStartedMigration;
case PipelineStatusCodes.Failed: // Failed: 'Failed',
return IconPathHelper.error;
case PipelineStatusCodes.Succeeded: // Succeeded: 'Succeeded',
return IconPathHelper.completedMigration;
// legacy status codes: Queued: 'Queued', InProgress: 'InProgress',Cancelled: 'Cancelled',
case PipelineStatusCodes.Queued:
return IconPathHelper.notStartedMigration;
case PipelineStatusCodes.InProgress:
return IconPathHelper.inProgressMigration;
case PipelineStatusCodes.Cancelled:
return IconPathHelper.cancel;
// default:
default:
return IconPathHelper.error;
}
}
export function getMigrationStatusImage(migration: azure.DatabaseMigration): IconPath {
const status = getMigrationStatus(migration);
switch (status) {
case MigrationStatus.InProgress:
return IconPathHelper.inProgressMigration;
@@ -223,7 +309,10 @@ export function getMigrationStatusImage(status: string): IconPath {
return IconPathHelper.notStartedMigration;
case MigrationStatus.Completing:
return IconPathHelper.completingCutover;
case MigrationStatus.Retriable:
return IconPathHelper.retry;
case MigrationStatus.Canceling:
case MigrationStatus.Canceled:
return IconPathHelper.cancel;
case MigrationStatus.Failed:
default:

View File

@@ -15,8 +15,84 @@ export enum SQLTargetAssetType {
SQLDB = 'Microsoft.Sql/servers',
}
export function getMigrationTargetType(migration: DatabaseMigration): string {
const id = migration.id?.toLowerCase();
export const ParallelCopyTypeCodes = {
None: 'None',
DynamicRange: 'DynamicRange',
PhysicalPartitionsOfTable: 'PhysicalPartitionsOfTable',
};
export const PipelineStatusCodes = {
// status codes: 'PreparingForCopy' | 'Copying' | 'CopyFinished' | 'RebuildingIndexes' | 'Succeeded' | 'Failed' | 'Canceled',
PreparingForCopy: 'PreparingForCopy',
Copying: 'Copying',
CopyFinished: 'CopyFinished',
RebuildingIndexes: 'RebuildingIndexes',
Succeeded: 'Succeeded',
Failed: 'Failed',
Canceled: 'Canceled',
// legacy status codes
Queued: 'Queued',
InProgress: 'InProgress',
Cancelled: 'Cancelled',
};
const _dateFormatter = new Intl.DateTimeFormat(
undefined, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
const _numberFormatter = new Intl.NumberFormat(
undefined, {
style: 'decimal',
useGrouping: true,
minimumIntegerDigits: 1,
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
export function formatDateTimeString(dateTime: string): string {
return dateTime
? _dateFormatter.format(new Date(dateTime))
: '';
}
export function formatTime(miliseconds: number): string {
if (miliseconds > 0) {
// hh:mm:ss
const matches = (new Date(miliseconds))?.toUTCString()?.match(/(\d\d:\d\d:\d\d)/) || [];
return matches?.length > 0 ? matches[0] : '';
}
return '';
}
export function formatNumber(value: number): string {
return value >= 0
? _numberFormatter.format(value)
: '';
}
export function formatCopyThroughPut(value: number): string {
return value >= 0
? loc.sizeFormatter.format(value / 1024)
: '';
}
export function formatSizeBytes(sizeBytes: number): string {
return formatSizeKb(sizeBytes / 1024);
}
export function formatSizeKb(sizeKb: number): string {
return loc.formatSizeMb(sizeKb / 1024);
}
export function getMigrationTargetType(migration: DatabaseMigration | undefined): string {
const id = migration?.id?.toLowerCase() || '';
if (id?.indexOf(SQLTargetAssetType.SQLMI.toLowerCase()) > -1) {
return loc.SQL_MANAGED_INSTANCE;
}
@@ -29,8 +105,8 @@ export function getMigrationTargetType(migration: DatabaseMigration): string {
return '';
}
export function getMigrationTargetTypeEnum(migration: DatabaseMigration): MigrationTargetType | undefined {
switch (migration.type) {
export function getMigrationTargetTypeEnum(migration: DatabaseMigration | undefined): MigrationTargetType | undefined {
switch (migration?.type) {
case SQLTargetAssetType.SQLMI:
return MigrationTargetType.SQLMI;
case SQLTargetAssetType.SQLVM:
@@ -42,37 +118,86 @@ export function getMigrationTargetTypeEnum(migration: DatabaseMigration): Migrat
}
}
export function getMigrationMode(migration: DatabaseMigration): string {
export function getMigrationMode(migration: DatabaseMigration | undefined): string {
return isOfflineMigation(migration)
? loc.OFFLINE
: loc.ONLINE;
}
export function getMigrationModeEnum(migration: DatabaseMigration): MigrationMode {
export function getMigrationModeEnum(migration: DatabaseMigration | undefined): MigrationMode {
return isOfflineMigation(migration)
? MigrationMode.OFFLINE
: MigrationMode.ONLINE;
}
export function isOfflineMigation(migration: DatabaseMigration): boolean {
return migration.properties.offlineConfiguration?.offline === true;
export function isOfflineMigation(migration: DatabaseMigration | undefined): boolean {
return migration?.properties?.offlineConfiguration?.offline === true;
}
export function isBlobMigration(migration: DatabaseMigration): boolean {
export function isBlobMigration(migration: DatabaseMigration | undefined): boolean {
return migration?.properties?.backupConfiguration?.sourceLocation?.fileStorageType === FileStorageType.AzureBlob;
}
export function getMigrationStatus(migration: DatabaseMigration): string {
return migration.properties.migrationStatus
?? migration.properties.provisioningState;
export function getMigrationStatus(migration: DatabaseMigration | undefined): string | undefined {
return migration?.properties.migrationStatus
?? migration?.properties.provisioningState;
}
export function hasMigrationOperationId(migration: DatabaseMigration | undefined): boolean {
const migrationId = migration?.id ?? '';
const migationOperationId = migration?.properties?.migrationOperationId ?? '';
return migrationId.length > 0
&& migationOperationId.length > 0;
}
export function canRetryMigration(status: string | undefined): boolean {
return status === undefined ||
status === MigrationStatus.Failed ||
status === MigrationStatus.Succeeded ||
status === MigrationStatus.Canceled;
export function canCancelMigration(migration: DatabaseMigration | undefined): boolean {
const status = getMigrationStatus(migration);
return hasMigrationOperationId(migration)
&& (status === MigrationStatus.InProgress ||
status === MigrationStatus.Retriable ||
status === MigrationStatus.Creating);
}
export function canDeleteMigration(migration: DatabaseMigration | undefined): boolean {
const status = getMigrationStatus(migration);
return status === MigrationStatus.Canceled
|| status === MigrationStatus.Failed
|| status === MigrationStatus.Retriable
|| status === MigrationStatus.Succeeded;
}
export function canRetryMigration(migration: DatabaseMigration | undefined): boolean {
const status = getMigrationStatus(migration);
return status === MigrationStatus.Canceled
|| status === MigrationStatus.Retriable
|| status === MigrationStatus.Failed
|| status === MigrationStatus.Succeeded;
}
export function canCutoverMigration(migration: DatabaseMigration | undefined): boolean {
const status = getMigrationStatus(migration);
return hasMigrationOperationId(migration)
&& status === MigrationStatus.InProgress
&& isOnlineMigration(migration)
&& isFullBackupRestored(migration);
}
export function isActiveMigration(migration: DatabaseMigration | undefined): boolean {
const status = getMigrationStatus(migration);
return status === MigrationStatus.Completing
|| status === MigrationStatus.Retriable
|| status === MigrationStatus.Creating
|| status === MigrationStatus.InProgress;
}
export function isFullBackupRestored(migration: DatabaseMigration | undefined): boolean {
const fileName = migration?.properties?.migrationStatusDetails?.lastRestoredFilename ?? '';
return migration?.properties?.migrationStatusDetails?.isFullBackupRestored
|| fileName.length > 0;
}
export function isOnlineMigration(migration: DatabaseMigration | undefined): boolean {
return getMigrationModeEnum(migration) === MigrationMode.ONLINE;
}
export function selectDatabasesFromList(selectedDbs: string[], databaseTableValues: azdata.DeclarativeTableCellValue[][]): azdata.DeclarativeTableCellValue[][] {

View File

@@ -45,6 +45,9 @@ export class IconPathHelper {
public static stop: IconPath;
public static view: IconPath;
public static sqlMigrationService: IconPath;
public static addNew: IconPath;
public static breadCrumb: IconPath;
public static allTables: IconPath;
public static setExtensionContext(context: vscode.ExtensionContext) {
IconPathHelper.copy = {
@@ -183,5 +186,17 @@ export class IconPathHelper {
light: context.asAbsolutePath('images/sqlMigrationService.svg'),
dark: context.asAbsolutePath('images/sqlMigrationService.svg'),
};
IconPathHelper.addNew = {
light: context.asAbsolutePath('images/addNew.svg'),
dark: context.asAbsolutePath('images/addNew.svg'),
};
IconPathHelper.breadCrumb = {
light: context.asAbsolutePath('images/breadCrumb.svg'),
dark: context.asAbsolutePath('images/breadCrumb.svg'),
};
IconPathHelper.allTables = {
light: context.asAbsolutePath('images/allTables.svg'),
dark: context.asAbsolutePath('images/allTables.svg'),
};
}
}

View File

@@ -8,6 +8,7 @@ import * as nls from 'vscode-nls';
import { EOL } from 'os';
import { MigrationStatus } from '../models/migrationLocalStorage';
import { MigrationSourceAuthenticationType } from '../models/stateMachine';
import { ParallelCopyTypeCodes, PipelineStatusCodes } from './helper';
const localize = nls.loadMessageBundle();
@@ -403,6 +404,8 @@ export const IR_PAGE_TITLE = localize('sql.migration.ir.page.title', "Azure Data
export const IR_PAGE_DESCRIPTION = localize('sql.migration.ir.page.description', "Azure Database Migration Service orchestrates database migration activities and tracks their progress. You can select an existing Database Migration Service as an Azure SQL target if you have created one previously, or create a new one below.");
export const SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR = localize('sql.migration.ir.page.sql.migration.service.not.found', "No Database Migration Service found. Create a new one.");
export const CREATE_NEW = localize('sql.migration.create.new', "Create new");
export const CREATE_NEW_MIGRATION_SERVICE = localize('sql.migration.create.new.migration.service', "Create new migration service");
export const CREATE_NEW_RESOURCE_GROUP = localize('sql.migration.create.new.resource.group', "Create new resource group");
export const INVALID_SERVICE_ERROR = localize('sql.migration.invalid.migration.service.error', "Select a valid Database Migration Service.");
export const SERVICE_OFFLINE_ERROR = localize('sql.migration.invalid.migration.service.offline.error', "Select a Database Migration Service that is connected to a node.");
export const AUTHENTICATION_KEYS = localize('sql.migration.authentication.types', "Authentication keys");
@@ -518,9 +521,9 @@ export const NOTEBOOK_SQL_MIGRATION_ASSESSMENT_TITLE = localize('sql.migration.s
export const NOTEBOOK_OPEN_ERROR = localize('sql.migration.notebook.open.error', "Failed to open the migration notebook.");
// Dashboard
export function DASHBOARD_REFRESH_MIGRATIONS(error: string): string {
return localize('sql.migration.refresh.migrations.error', "An error occurred while refreshing the migrations list: '{0}'. Please check your linked Azure connection and click refresh to try again.", error);
}
export const DASHBOARD_REFRESH_MIGRATIONS_TITLE = localize('sql.migration.refresh.migrations.error.title', 'An error has occured while refreshing the migrations list.');
export const DASHBOARD_REFRESH_MIGRATIONS_LABEL = localize('sql.migration.refresh.migrations.error.label', "An error occurred while refreshing the migrations list. Please check your linked Azure connection and click refresh to try again.");
export const DASHBOARD_TITLE = localize('sql.migration.dashboard.title', "Azure SQL Migration");
export const DASHBOARD_DESCRIPTION = localize('sql.migration.dashboard.description', "Determine the migration readiness of your SQL Server instances, identify a recommended Azure SQL target, and complete the migration of your SQL Server instance to Azure SQL Managed Instance or SQL Server on Azure Virtual Machines.");
export const DASHBOARD_MIGRATE_TASK_BUTTON_TITLE = localize('sql.migration.dashboard.migrate.task.button', "Migrate to Azure SQL");
@@ -547,6 +550,7 @@ export function MIGRATION_INPROGRESS_WARNING(count: number) {
export const FEEDBACK_ISSUE_TITLE = localize('sql.migration.feedback.issue.title', "Feedback on the migration experience");
//Migration cutover dialog
export const BREADCRUMB_MIGRATIONS = localize('sql.migration.details.breadcrumb.migrations', 'Migrations');
export const MIGRATION_CUTOVER = localize('sql.migration.cutover', "Migration cutover");
export const COMPLETE_CUTOVER = localize('sql.migration.complete.cutover', "Complete cutover");
export const SOURCE_DATABASE = localize('sql.migration.source.database', "Source database name");
@@ -588,6 +592,16 @@ export const NA = localize('sql.migration.na', "N/A");
export const EMPTY_TABLE_TEXT = localize('sql.migration.empty.table.text', "No backup files");
export const EMPTY_TABLE_SUBTEXT = localize('sql.migration.empty.table.subtext', "If results were expected, verify the connection to the SQL Server instance.");
export const MIGRATION_CUTOVER_ERROR = localize('sql.migration.cutover.error', 'An error occurred while initiating cutover.');
export const REFRESH_BUTTON_TEXT = localize('sql.migration.details.refresh', 'Refresh');
export const SERVER_OBJECTS_FIELD_LABEL = localize('sql.migration.details.serverobjects.field.label', 'Server objects');
export const SERVER_OBJECTS_LABEL = localize('sql.migration.details.serverobjects.label', 'Server objects');
export const SERVER_OBJECTS_ALL_TABLES_LABEL = localize('sql.migration.details.serverobjects.all.tables.label', 'Total tables');
export const SERVER_OBJECTS_IN_PROGRESS_TABLES_LABEL = localize('sql.migration.details.serverobjects.inprogress.tables.label', 'In progress');
export const SERVER_OBJECTS_SUCCESSFUL_TABLES_LABEL = localize('sql.migration.details.serverobjects.successful.tables.label', 'Successful');
export const SERVER_OBJECTS_FAILED_TABLES_LABEL = localize('sql.migration.details.serverobjects.failed.tables.label', 'Failed');
export const SERVER_OBJECTS_CANCELLED_TABLES_LABEL = localize('sql.migration.details.serverobjects.cancelled.tables.label', 'Cancelled');
export const FILTER_SERVER_OBJECTS_PLACEHOLDER = localize('sql.migration.details.serverobjects.filter.label', 'Filter table migration results');
export const FILTER_SERVER_OBJECTS_ARIA_LABEL = localize('sql.migration.details.serverobjects.filter.aria.label', 'Filter table migration results using keywords');
//Migration confirm cutover dialog
export const COMPLETING_CUTOVER_WARNING = localize('sql.migration.completing.cutover.warning', "Completing cutover without restoring all the backups may result in a data loss.");
@@ -616,6 +630,7 @@ export const MIGRATION_CANNOT_CUTOVER = localize('sql.migration.cannot.cutover',
export const FILE_NAME = localize('sql.migration.file.name', "File name");
export const SIZE_COLUMN_HEADER = localize('sql.migration.size.column.header', "Size");
export const NO_PENDING_BACKUPS = localize('sql.migration.no.pending.backups', "No pending backups. Click refresh to check current status.");
//Migration status dialog
export const ADD_ACCOUNT = localize('sql.migration.status.add.account', "Add account");
export const ADD_ACCOUNT_MESSAGE = localize('sql.migration.status.add.account.MESSAGE', "Add your Azure account to view existing migrations and their status.");
@@ -625,11 +640,14 @@ export const STATUS_ONGOING = localize('sql.migration.status.dropdown.ongoing',
export const STATUS_COMPLETING = localize('sql.migration.status.dropdown.completing', "Status: Completing");
export const STATUS_SUCCEEDED = localize('sql.migration.status.dropdown.succeeded', "Status: Succeeded");
export const STATUS_FAILED = localize('sql.migration.status.dropdown.failed', "Status: Failed");
export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migration', "Search for migrations");
export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migration', "Filter migration results");
export const ONLINE = localize('sql.migration.online', "Online");
export const OFFLINE = localize('sql.migration.offline', "Offline");
export const DATABASE = localize('sql.migration.database', "Database");
export const STATUS_COLUMN = localize('sql.migration.database.status.column', "Status");
export const SRC_DATABASE = localize('sql.migration.src.database', "Source database");
export const SRC_SERVER = localize('sql.migration.src.server', "Source name");
export const STATUS_COLUMN = localize('sql.migration.database.status.column', "Migration status");
export const DATABASE_MIGRATION_SERVICE = localize('sql.migration.database.migration.service', "Database Migration Service");
export const DURATION = localize('sql.migration.duration', "Duration");
export const AZURE_SQL_TARGET = localize('sql.migration.azure.sql.target', "Target type");
@@ -637,7 +655,9 @@ export const SQL_MANAGED_INSTANCE = localize('sql.migration.sql.managed.instance
export const SQL_VIRTUAL_MACHINE = localize('sql.migration.sql.virtual.machine', "SQL Virtual Machine");
export const SQL_DATABASE = localize('sql.migration.sql.database', "SQL Database");
export const TARGET_AZURE_SQL_INSTANCE_NAME = localize('sql.migration.target.azure.sql.instance.name', "Target name");
export const MIGRATION_MODE = localize('sql.migration.cutover.type', "Migration mode");
export const TARGET_SERVER_COLUMN = localize('sql.migration.target.azure.sql.instance.server.name', "Target name");
export const TARGET_DATABASE_COLUMN = localize('sql.migration.target.azure.sql.instance.database.name', "Target database");
export const MIGRATION_MODE = localize('sql.migration.cutover.type', "Mode");
export const START_TIME = localize('sql.migration.start.time', "Start time");
export const FINISH_TIME = localize('sql.migration.finish.time', "Finish time");
@@ -648,20 +668,53 @@ export function STATUS_VALUE(status: string, count: number): string {
return localize('sql.migration.status.error.count.none', "{0}", StatusLookup[status] ?? status);
}
export const MIGRATION_ERROR_DETAILS_TITLE = localize('sql.migration.error.details.title', "Migration error details");
export const MIGRATION_ERROR_DETAILS_LABEL = localize('sql.migration.error.details.label', "Migration error(s))");
export const OPEN_MIGRATION_DETAILS_ERROR = localize('sql.migration.open.migration.destails.error', "Error opening migration details dialog");
export const OPEN_MIGRATION_TARGET_ERROR = localize('sql.migration.open.migration.target.error', "Error opening migration target");
export const OPEN_MIGRATION_SERVICE_ERROR = localize('sql.migration.open.migration.service.error', "Error opening migration service dialog");
export const LOAD_MIGRATION_LIST_ERROR = localize('sql.migration.load.migration.list.error', "Error loading migrations list");
export const ERROR_DIALOG_CLEAR_BUTTON_LABEL = localize('sql.migration.error.dialog.clear.button.label', "Clear");
export interface LookupTable<T> {
[key: string]: T;
}
export const StatusLookup: LookupTable<string | undefined> = {
['InProgress']: localize('sql.migration.status.inprogress', 'In progress'),
['Succeeded']: localize('sql.migration.status.succeeded', 'Succeeded'),
['Creating']: localize('sql.migration.status.creating', 'Creating'),
['Completing']: localize('sql.migration.status.completing', 'Completing'),
['Canceling']: localize('sql.migration.status.canceling', 'Canceling'),
['Failed']: localize('sql.migration.status.failed', 'Failed'),
[MigrationStatus.InProgress]: localize('sql.migration.status.inprogress', 'In progress'),
[MigrationStatus.Succeeded]: localize('sql.migration.status.succeeded', 'Succeeded'),
[MigrationStatus.Creating]: localize('sql.migration.status.creating', 'Creating'),
[MigrationStatus.Completing]: localize('sql.migration.status.completing', 'Completing'),
[MigrationStatus.Retriable]: localize('sql.migration.status.retriable', 'Retriable'),
[MigrationStatus.Canceling]: localize('sql.migration.status.canceling', 'Canceling'),
[MigrationStatus.Canceled]: localize('sql.migration.status.canceled', 'Canceled'),
[MigrationStatus.Failed]: localize('sql.migration.status.failed', 'Failed'),
default: undefined
};
export const PipelineRunStatus: LookupTable<string | undefined> = {
// status codes: ['PreparingForCopy' | 'Copying' | 'CopyFinished' | 'RebuildingIndexes' | 'Succeeded' | 'Failed' | 'Canceled']
[PipelineStatusCodes.PreparingForCopy]: localize('sql.migration.copy.status.preparingforcopy', 'Preparing'),
[PipelineStatusCodes.Copying]: localize('sql.migration.copy.status.copying', 'Copying'),
[PipelineStatusCodes.CopyFinished]: localize('sql.migration.copy.status.copyfinished', 'Copy finished'),
[PipelineStatusCodes.RebuildingIndexes]: localize('sql.migration.copy.status.rebuildingindexes', 'Rebuilding indexes'),
[PipelineStatusCodes.Succeeded]: localize('sql.migration.copy.status.succeeded', 'Succeeded'),
[PipelineStatusCodes.Failed]: localize('sql.migration.copy.status.failed', 'Failed'),
[PipelineStatusCodes.Canceled]: localize('sql.migration.copy.status.canceled', 'Canceled'),
// legacy status codes ['Queued', 'InProgress', 'Cancelled']
[PipelineStatusCodes.Queued]: localize('sql.migration.copy.status.queued', 'Queued'),
[PipelineStatusCodes.InProgress]: localize('sql.migration.copy.status.inprogress', 'In progress'),
[PipelineStatusCodes.Cancelled]: localize('sql.migration.copy.status.cancelled', 'Cancelled'),
};
export const ParallelCopyType: LookupTable<string | undefined> = {
[ParallelCopyTypeCodes.None]: localize('sql.migration.parallel.copy.type.none', 'None'),
[ParallelCopyTypeCodes.PhysicalPartitionsOfTable]: localize('sql.migration.parallel.copy.type.physical', 'Physical partitions'),
[ParallelCopyTypeCodes.DynamicRange]: localize('sql.migration.parallel.copy.type.dynamic', 'Dynamic range'),
};
export function STATUS_WARNING_COUNT(status: string, count: number): string | undefined {
if (status === MigrationStatus.InProgress ||
status === MigrationStatus.Creating ||
@@ -699,6 +752,27 @@ export function SEC(sec: number): string {
return localize('sql.migration.sec', "{0} sec", sec);
}
export const sizeFormatter = new Intl.NumberFormat(
undefined, {
style: 'decimal',
useGrouping: true,
minimumIntegerDigits: 1,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
export function formatSizeMb(sizeMb: number): string {
if (isNaN(sizeMb) || sizeMb < 0) {
return '';
} else if (sizeMb < 1024) {
return localize('sql.migration.size.mb', "{0} MB", sizeFormatter.format(sizeMb));
} else if (sizeMb < 1024 * 1024) {
return localize('sql.migration.size.gb', "{0} GB", sizeFormatter.format(sizeMb / 1024));
} else {
return localize('sql.migration.size.tb', "{0} TB", sizeFormatter.format(sizeMb / 1024 / 1024));
}
}
// SQL Migration Service Details page.
export const SQL_MIGRATION_SERVICE_DETAILS_SUB_TITLE = localize('sql.migration.service.details.dialog.title', "Azure Database Migration Service");
export const SQL_MIGRATION_SERVICE_DETAILS_BUTTON_LABEL = localize('sql.migration.service.details.button.label', "Close");
@@ -761,6 +835,9 @@ export function WARNINGS_COUNT(totalCount: number): string {
export const AUTHENTICATION_TYPE = localize('sql.migration.authentication.type', "Authentication type");
export const REFRESH_BUTTON_LABEL = localize('sql.migration.status.refresh.label', 'Refresh');
export const STATUS_LABEL = localize('sql.migration.status.status.label', 'Status');
export const SORT_LABEL = localize('sql.migration.migration.list.sort.label', 'Sort');
export const ASCENDING_LABEL = localize('sql.migration.migration.list.ascending.label', 'Ascending');
// Saved Assessment Dialog
export const NEXT_LABEL = localize('sql.migration.saved.assessment.next', "Next");
@@ -786,3 +863,46 @@ export function MIGRATION_SERVICE_SERVICE_PROMPT(serviceName: string): string {
return localize('sql.migration.service.prompt', '{0} (change)', serviceName);
}
export const MIGRATION_SERVICE_DESCRIPTION = localize('sql.migration.select.service.description', 'Azure Database Migration Service');
// Desktop tabs
export const DESKTOP_MIGRATION_BUTTON_LABEL = localize('sql.migration.tab.button.migration.label', 'New migration');
export const DESKTOP_MIGRATION_BUTTON_DESCRIPTION = localize('sql.migration.tab.button.migration.description', 'Migrate to Azure SQL');
export const DESKTOP_SUPPORT_BUTTON_LABEL = localize('sql.migration.tab.button.support.label', 'New support request');
export const DESKTOP_SUPPORT_BUTTON_DESCRIPTION = localize('sql.migration.tab.button.support.description', 'New support request');
export const DESKTOP_FEEDBACK_BUTTON_LABEL = localize('sql.migration.tab.button.feedback.label', 'Feedback');
export const DESKTOP_FEEDBACK_BUTTON_DESCRIPTION = localize('sql.migration.tab.button.feedback.description', 'Feedback');
export const DESKTOP_DASHBOARD_TAB_TITLE = localize('sql.migration.tab.dashboard.title', 'Dashboard');
export const DESKTOP_MIGRATIONS_TAB_TITLE = localize('sql.migration.tab.migrations.title', 'Migrations');
// dashboard tab
export const DASHBOARD_HELP_LINK_MIGRATE_USING_ADS = localize('sql.migration.dashboard.help.link.migrateUsingADS', 'Migrate databases using Azure Data Studio');
export const DASHBOARD_HELP_DESCRIPTION_MIGRATE_USING_ADS = localize('sql.migration.dashboard.help.description.migrateUsingADS', 'The Azure SQL Migration extension for Azure Data Studio provides capabilities to assess, get right-sized Azure recommendations and migrate SQL Server databases to Azure.');
export const DASHBOARD_HELP_LINK_MI_TUTORIAL = localize('sql.migration.dashboard.help.link.mi', 'Tutorial: Migrate to Azure SQL Managed Instance (online)');
export const DASHBOARD_HELP_DESCRIPTION_MI_TUTORIAL = localize('sql.migration.dashboard.help.description.mi', 'A step-by-step tutorial to migrate databases from a SQL Server instance (on-premises or Azure Virtual Machines) to Azure SQL Managed Instance with minimal downtime.');
export const DASHBOARD_HELP_LINK_VM_TUTORIAL = localize('sql.migration.dashboard.help.link.vm', 'Tutorial: Migrate to SQL Server on Azure Virtual Machines (online)');
export const DASHBOARD_HELP_DESCRIPTION_VMTUTORIAL = localize('sql.migration.dashboard.help.description.vm', 'A step-by-step tutorial to migrate databases from a SQL Server instance (on-premises) to SQL Server on Azure Virtual Machines with minimal downtime.');
export const DASHBOARD_HELP_LINK_DMS_GUIDE = localize('sql.migration.dashboard.help.link.dmsGuide', 'Azure Database Migration Guides');
export const DASHBOARD_HELP_DESCRIPTION_DMS_GUIDE = localize('sql.migration.dashboard.help.description.dmsGuide', 'A hub of migration articles that provides step-by-step guidance for migrating and modernizing your data assets in Azure.');
// Error info
export const DATABASE_MIGRATION_STATUS_TITLE = localize('sql.migration.error.title', 'Migration status details');
export const TABLE_MIGRATION_STATUS_TITLE = localize('sql.migration.table.error.title', 'Table migration status details');
export function DATABASE_MIGRATION_STATUS_LABEL(status?: string): string {
return localize('sql.migration.database.migration.status.label', 'Database migration status: {0}', status ?? '');
}
export function TABLE_MIGRATION_STATUS_LABEL(status?: string): string {
return localize('sql.migration.table.migration.status.label', 'Table migration status: {0}', status ?? '');
}
export const SQLDB_COL_TABLE_NAME = localize('sql.migration.sqldb.column.tablename', 'Table name');
export const SQLDB_COL_DATA_READ = localize('sql.migration.sqldb.column.dataread', 'Data read');
export const SQLDB_COL_DATA_WRITTEN = localize('sql.migration.sqldb.column.datawritten', 'Data written');
export const SQLDB_COL_ROWS_READ = localize('sql.migration.sqldb.column.rowsread', 'Rows read');
export const SQLDB_COL_ROWS_COPIED = localize('sql.migration.sqldb.column.rowscopied', 'Rows copied');
export const SQLDB_COL_COPY_THROUGHPUT = localize('sql.migration.sqldb.column.copythroughput', 'Copy throughput');
export const SQLDB_COL_COPY_DURATION = localize('sql.migration.sqldb.column.copyduration', 'Copy duration');
export const SQLDB_COL_PARRALEL_COPY_TYPE = localize('sql.migration.sqldb.column.parallelcopytype', 'Parallel copy type');
export const SQLDB_COL_USED_PARALLEL_COPIES = localize('sql.migration.sqldb.column.usedparallelcopies', 'Used parallel copies');
export const SQLDB_COL_COPY_START = localize('sql.migration.sqldb.column.copystart', 'Copy start');

View File

@@ -0,0 +1,789 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
import * as styles from '../constants/styles';
import * as loc from '../constants/strings';
import { filterMigrations } from '../api/utils';
import { DatabaseMigration } from '../api/azure';
import { getCurrentMigrations, getSelectedServiceStatus, isServiceContextValid, MigrationLocalStorage } from '../models/migrationLocalStorage';
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
import { logError, TelemetryViews } from '../telemtery';
import { AdsMigrationStatus, MenuCommands, TabBase } from './tabBase';
import { DashboardStatusBar } from './sqlServerDashboard';
interface IActionMetadata {
title?: string,
description?: string,
link?: string,
iconPath?: azdata.ThemedIconPath,
command?: string;
}
interface StatusCard {
container: azdata.DivContainer;
count: azdata.TextComponent,
textContainer?: azdata.FlexContainer,
warningContainer?: azdata.FlexContainer,
warningText?: azdata.TextComponent,
}
export const DashboardTabId = 'DashboardTab';
const maxWidth = 800;
const BUTTON_CSS = {
'font-size': '13px',
'line-height': '18px',
'margin': '4px 0',
'text-align': 'left',
};
export class DashboardTab extends TabBase<DashboardTab> {
private _migrationStatusCardsContainer!: azdata.FlexContainer;
private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent;
private _inProgressMigrationButton!: StatusCard;
private _inProgressWarningMigrationButton!: StatusCard;
private _allMigrationButton!: StatusCard;
private _successfulMigrationButton!: StatusCard;
private _failedMigrationButton!: StatusCard;
private _completingMigrationButton!: StatusCard;
private _selectServiceText!: azdata.TextComponent;
private _serviceContextButton!: azdata.ButtonComponent;
private _refreshButton!: azdata.ButtonComponent;
constructor() {
super();
this.title = loc.DESKTOP_DASHBOARD_TAB_TITLE;
this.id = DashboardTabId;
this.icon = IconPathHelper.sqlMigrationLogo;
}
public onDialogClosed = async (): Promise<void> =>
await this.updateServiceContext(this._serviceContextButton);
public async create(
view: azdata.ModelView,
openMigrationsFcn: (status: AdsMigrationStatus) => Promise<void>,
statusBar: DashboardStatusBar): Promise<DashboardTab> {
this.view = view;
this.openMigrationFcn = openMigrationsFcn;
this.statusBar = statusBar;
await this.initialize(this.view);
return this;
}
public async refresh(): Promise<void> {
if (this.isRefreshing) {
return;
}
this.isRefreshing = true;
this._migrationStatusCardLoadingContainer.loading = true;
let migrations: DatabaseMigration[] = [];
try {
await this.statusBar.clearError();
migrations = await getCurrentMigrations();
} catch (e) {
await this.statusBar.showError(
loc.DASHBOARD_REFRESH_MIGRATIONS_TITLE,
loc.DASHBOARD_REFRESH_MIGRATIONS_LABEL,
e.message);
logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', e);
}
const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
let warningCount = 0;
for (let i = 0; i < inProgressMigrations.length; i++) {
if (inProgressMigrations[i].properties.migrationFailureError?.message ||
inProgressMigrations[i].properties.migrationStatusDetails?.fileUploadBlockingErrors ||
inProgressMigrations[i].properties.migrationStatusDetails?.restoreBlockingReason) {
warningCount += 1;
}
}
if (warningCount > 0) {
this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount);
this._inProgressMigrationButton.container.display = 'none';
this._inProgressWarningMigrationButton.container.display = '';
} else {
this._inProgressMigrationButton.container.display = '';
this._inProgressWarningMigrationButton.container.display = 'none';
}
this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString();
this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString();
this._updateStatusCard(migrations, this._successfulMigrationButton, AdsMigrationStatus.SUCCEEDED, true);
this._updateStatusCard(migrations, this._failedMigrationButton, AdsMigrationStatus.FAILED);
this._updateStatusCard(migrations, this._completingMigrationButton, AdsMigrationStatus.COMPLETING);
this._updateStatusCard(migrations, this._allMigrationButton, AdsMigrationStatus.ALL, true);
await this._updateSummaryStatus();
this.isRefreshing = false;
this._migrationStatusCardLoadingContainer.loading = false;
}
protected async initialize(view: azdata.ModelView): Promise<void> {
const container = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column',
width: '100%',
height: '100%'
}).component();
const toolbar = view.modelBuilder.toolbarContainer();
toolbar.addToolbarItems([
<azdata.ToolbarComponent>{ component: this.createNewMigrationButton() },
<azdata.ToolbarComponent>{ component: this.createNewSupportRequestButton() },
<azdata.ToolbarComponent>{ component: this.createFeedbackButton() },
]);
container.addItem(
toolbar.component(),
{ CSSStyles: { 'flex': '0 0 auto' } });
const header = this._createHeader(view);
// Files need to have the vscode-file scheme to be loaded by ADS
const watermarkUri = vscode.Uri
.file(<string>IconPathHelper.migrationDashboardHeaderBackground.light)
.with({ scheme: 'vscode-file' });
container.addItem(header, {
CSSStyles: {
'background-image': `
url(${watermarkUri}),
linear-gradient(0deg, rgba(0, 0, 0, 0.05) 0%, rgba(0, 0, 0, 0) 100%)`,
'background-repeat': 'no-repeat',
'background-position': '91.06% 100%',
'margin-bottom': '20px'
}
});
const tasksContainer = await this._createTasks(view);
header.addItem(tasksContainer, {
CSSStyles: {
'width': `${maxWidth}px`,
'margin': '24px'
}
});
container.addItem(
await this._createFooter(view),
{ CSSStyles: { 'margin': '0 24px' } });
this.content = container;
}
private _createHeader(view: azdata.ModelView): azdata.FlexContainer {
const header = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column',
width: maxWidth,
}).component();
const titleComponent = view.modelBuilder.text()
.withProps({
value: loc.DASHBOARD_TITLE,
width: '750px',
CSSStyles: { ...styles.DASHBOARD_TITLE_CSS }
}).component();
const descriptionComponent = view.modelBuilder.text()
.withProps({
value: loc.DASHBOARD_DESCRIPTION,
CSSStyles: { ...styles.NOTE_CSS }
}).component();
header.addItems([titleComponent, descriptionComponent], {
CSSStyles: {
'width': `${maxWidth}px`,
'padding-left': '24px'
}
});
return header;
}
private async _createTasks(view: azdata.ModelView): Promise<azdata.Component> {
const tasksContainer = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'row',
width: '100%',
}).component();
const migrateButtonMetadata: IActionMetadata = {
title: loc.DASHBOARD_MIGRATE_TASK_BUTTON_TITLE,
description: loc.DASHBOARD_MIGRATE_TASK_BUTTON_DESCRIPTION,
iconPath: IconPathHelper.sqlMigrationLogo,
command: MenuCommands.StartMigration
};
const preRequisiteListTitle = view.modelBuilder.text()
.withProps({
value: loc.PRE_REQ_TITLE,
CSSStyles: {
...styles.BODY_CSS,
'margin': '0px',
}
}).component();
const migrateButton = this._createTaskButton(view, migrateButtonMetadata);
const preRequisiteListElement = view.modelBuilder.text()
.withProps({
value: [
loc.PRE_REQ_1,
loc.PRE_REQ_2,
loc.PRE_REQ_3
],
CSSStyles: {
...styles.SMALL_NOTE_CSS,
'padding-left': '12px',
'margin': '-0.5em 0px',
}
}).component();
const preRequisiteLearnMoreLink = view.modelBuilder.hyperlink()
.withProps({
label: loc.LEARN_MORE,
ariaLabel: loc.LEARN_MORE_ABOUT_PRE_REQS,
url: 'https://aka.ms/azuresqlmigrationextension',
}).component();
const preReqContainer = view.modelBuilder.flexContainer()
.withItems([
preRequisiteListTitle,
preRequisiteListElement,
preRequisiteLearnMoreLink])
.withLayout({ flexFlow: 'column' })
.component();
tasksContainer.addItem(migrateButton, {});
tasksContainer.addItems(
[preReqContainer],
{ CSSStyles: { 'margin-left': '20px' } });
return tasksContainer;
}
private _createTaskButton(view: azdata.ModelView, taskMetaData: IActionMetadata): azdata.Component {
const maxHeight: number = 84;
const maxWidth: number = 236;
const buttonContainer = view.modelBuilder.button().withProps({
buttonType: azdata.ButtonType.Informational,
description: taskMetaData.description,
height: maxHeight,
iconHeight: 32,
iconPath: taskMetaData.iconPath,
iconWidth: 32,
label: taskMetaData.title,
title: taskMetaData.title,
width: maxWidth,
CSSStyles: {
'border': '1px solid',
'display': 'flex',
'flex-direction': 'column',
'justify-content': 'flex-start',
'border-radius': '4px',
'transition': 'all .5s ease',
}
}).component();
this.disposables.push(
buttonContainer.onDidClick(async () => {
if (taskMetaData.command) {
await vscode.commands.executeCommand(taskMetaData.command);
}
}));
return view.modelBuilder.divContainer().withItems([buttonContainer]).component();
}
private async _createFooter(view: azdata.ModelView): Promise<azdata.Component> {
const footerContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'row',
width: maxWidth,
justifyContent: 'flex-start'
}).component();
const statusContainer = await this._createMigrationStatusContainer(view);
const videoLinksContainer = this._createVideoLinks(view);
footerContainer.addItem(statusContainer);
footerContainer.addItem(
videoLinksContainer,
{ CSSStyles: { 'padding-left': '8px', } });
return footerContainer;
}
private _createVideoLinks(view: azdata.ModelView): azdata.Component {
const linksContainer = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column',
width: '440px',
height: '365px',
justifyContent: 'flex-start',
}).withProps({
CSSStyles: {
'border': '1px solid rgba(0, 0, 0, 0.1)',
'padding': '10px',
'overflow': 'scroll',
}
}).component();
const titleComponent = view.modelBuilder.text()
.withProps({
value: loc.HELP_TITLE,
CSSStyles: { ...styles.SECTION_HEADER_CSS }
})
.component();
linksContainer.addItems(
[titleComponent],
{ CSSStyles: { 'margin-bottom': '16px' } });
const links = [
{
title: loc.DASHBOARD_HELP_LINK_MIGRATE_USING_ADS,
description: loc.DASHBOARD_HELP_DESCRIPTION_MIGRATE_USING_ADS,
link: 'https://docs.microsoft.com/azure/dms/migration-using-azure-data-studio'
},
{
title: loc.DASHBOARD_HELP_LINK_MI_TUTORIAL,
description: loc.DASHBOARD_HELP_DESCRIPTION_MI_TUTORIAL,
link: 'https://docs.microsoft.com/azure/dms/tutorial-sql-server-managed-instance-online-ads'
},
{
title: loc.DASHBOARD_HELP_LINK_VM_TUTORIAL,
description: loc.DASHBOARD_HELP_DESCRIPTION_VMTUTORIAL,
link: 'https://docs.microsoft.com/azure/dms/tutorial-sql-server-to-virtual-machine-online-ads'
},
{
title: loc.DASHBOARD_HELP_LINK_DMS_GUIDE,
description: loc.DASHBOARD_HELP_DESCRIPTION_DMS_GUIDE,
link: 'https://docs.microsoft.com/data-migration/'
},
];
linksContainer.addItems(links.map(l => this._createLink(view, l)), {});
const videoLinks: IActionMetadata[] = [];
const videosContainer = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'row',
width: maxWidth,
}).component();
videosContainer.addItems(videoLinks.map(l => this._createVideoLink(view, l)), {});
linksContainer.addItem(videosContainer);
return linksContainer;
}
private _createLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
const maxWidth = 400;
const labelsContainer = view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
'flex-direction': 'column',
'width': `${maxWidth}px`,
'justify-content': 'flex-start',
'margin-bottom': '12px'
}
}).component();
const linkContainer = view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
'flex-direction': 'row',
'width': `${maxWidth}px`,
'justify-content': 'flex-start',
'margin-bottom': '4px'
}
}).component();
const descriptionComponent = view.modelBuilder.text()
.withProps({
value: linkMetaData.description,
width: maxWidth,
CSSStyles: { ...styles.NOTE_CSS }
}).component();
const linkComponent = view.modelBuilder.hyperlink()
.withProps({
label: linkMetaData.title!,
url: linkMetaData.link!,
showLinkIcon: true,
CSSStyles: { ...styles.BODY_CSS }
}).component();
linkContainer.addItem(linkComponent);
labelsContainer.addItems([linkContainer, descriptionComponent]);
return labelsContainer;
}
private _createVideoLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
const maxWidth = 150;
const videosContainer = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column',
width: maxWidth,
justifyContent: 'flex-start'
}).component();
const video1Container = view.modelBuilder.divContainer()
.withProps({
clickable: true,
width: maxWidth,
height: '100px'
}).component();
const descriptionComponent = view.modelBuilder.text()
.withProps({
value: linkMetaData.description,
width: maxWidth,
height: '50px',
CSSStyles: { ...styles.BODY_CSS }
}).component();
this.disposables.push(
video1Container.onDidClick(async () => {
if (linkMetaData.link) {
await vscode.env.openExternal(vscode.Uri.parse(linkMetaData.link));
}
}));
videosContainer.addItem(video1Container, {
CSSStyles: {
'background-image': `url(${vscode.Uri.file(<string>linkMetaData.iconPath?.light)})`,
'background-repeat': 'no-repeat',
'background-position': 'top',
'width': `${maxWidth}px`,
'height': '104px',
'background-size': `${maxWidth}px 120px`
}
});
videosContainer.addItem(descriptionComponent);
return videosContainer;
}
private _createStatusCard(
view: azdata.ModelView,
cardIconPath: IconPath,
cardTitle: string,
hasSubtext: boolean = false
): StatusCard {
const buttonWidth = '400px';
const buttonHeight = hasSubtext ? '70px' : '50px';
const statusCard = view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
'width': buttonWidth,
'height': buttonHeight,
'align-items': 'center',
}
}).component();
const statusIcon = view.modelBuilder.image()
.withProps({
iconPath: cardIconPath!.light,
iconHeight: 24,
iconWidth: 24,
height: 32,
CSSStyles: { 'margin': '0 8px' }
}).component();
const textContainer = view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
const cardTitleText = view.modelBuilder.text()
.withProps({ value: cardTitle })
.withProps({
CSSStyles: {
...styles.SECTION_HEADER_CSS,
'width': '240px',
}
}).component();
textContainer.addItem(cardTitleText);
const cardCount = view.modelBuilder.text()
.withProps({
value: '0',
CSSStyles: {
...styles.BIG_NUMBER_CSS,
'margin': '0 0 0 8px',
'text-align': 'center',
}
}).component();
let warningContainer;
let warningText;
if (hasSubtext) {
const warningIcon = view.modelBuilder.image()
.withProps({
iconPath: IconPathHelper.warning,
iconWidth: 12,
iconHeight: 12,
width: 12,
height: 18,
}).component();
const warningDescription = '';
warningText = view.modelBuilder.text()
.withProps({
value: warningDescription,
CSSStyles: {
...styles.BODY_CSS,
'padding-left': '8px',
}
}).component();
warningContainer = view.modelBuilder.flexContainer()
.withItems(
[warningIcon, warningText],
{ flex: '0 0 auto' })
.withProps({ CSSStyles: { 'align-items': 'center' } })
.component();
textContainer.addItem(warningContainer);
}
statusCard.addItems([
statusIcon,
textContainer,
cardCount,
]);
const compositeButton = view.modelBuilder.divContainer()
.withItems([statusCard])
.withProps({
ariaRole: 'button',
ariaLabel: loc.SHOW_STATUS,
clickable: true,
CSSStyles: {
'height': buttonHeight,
'margin-bottom': '16px',
'border': '1px solid',
'display': 'flex',
'flex-direction': 'column',
'justify-content': 'flex-start',
'border-radius': '4px',
'transition': 'all .5s ease',
}
}).component();
return {
container: compositeButton,
count: cardCount,
textContainer: textContainer,
warningContainer: warningContainer,
warningText: warningText
};
}
private async _createMigrationStatusContainer(view: azdata.ModelView): Promise<azdata.FlexContainer> {
const statusContainer = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column',
width: '400px',
height: '365px',
justifyContent: 'flex-start',
})
.withProps({
CSSStyles: {
'border': '1px solid rgba(0, 0, 0, 0.1)',
'padding': '10px',
}
})
.component();
const statusContainerTitle = view.modelBuilder.text()
.withProps({
value: loc.DATABASE_MIGRATION_STATUS,
width: '100%',
CSSStyles: { ...styles.SECTION_HEADER_CSS }
}).component();
this._refreshButton = view.modelBuilder.button()
.withProps({
label: loc.REFRESH,
iconPath: IconPathHelper.refresh,
iconHeight: 16,
iconWidth: 16,
width: 70,
CSSStyles: { 'float': 'right' }
}).component();
const statusHeadingContainer = view.modelBuilder.flexContainer()
.withItems([
statusContainerTitle,
this._refreshButton,
]).withLayout({
alignContent: 'center',
alignItems: 'center',
flexFlow: 'row',
}).component();
this.disposables.push(
this._refreshButton.onDidClick(async (e) => {
this._refreshButton.enabled = false;
await this.refresh();
this._refreshButton.enabled = true;
}));
const buttonContainer = view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
'justify-content': 'left',
'align-iems': 'center',
},
})
.component();
buttonContainer.addItem(
await this._createServiceSelector(view));
this._selectServiceText = view.modelBuilder.text()
.withProps({
value: loc.SELECT_SERVICE_MESSAGE,
CSSStyles: {
'font-size': '12px',
'margin': '10px',
'font-weight': '350',
'text-align': 'center',
'display': 'none'
}
}).component();
const header = view.modelBuilder.flexContainer()
.withItems([statusHeadingContainer, buttonContainer])
.withLayout({ flexFlow: 'column', })
.component();
this._migrationStatusCardsContainer = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column',
height: '272px',
})
.withProps({ CSSStyles: { 'overflow': 'hidden auto' } })
.component();
await this._updateSummaryStatus();
// in progress
this._inProgressMigrationButton = this._createStatusCard(
view,
IconPathHelper.inProgressMigration,
loc.MIGRATION_IN_PROGRESS);
this.disposables.push(
this._inProgressMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ONGOING)));
this._migrationStatusCardsContainer.addItem(
this._inProgressMigrationButton.container,
{ flex: '0 0 auto' });
// in progress warning
this._inProgressWarningMigrationButton = this._createStatusCard(
view,
IconPathHelper.inProgressMigration,
loc.MIGRATION_IN_PROGRESS,
true);
this.disposables.push(
this._inProgressWarningMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ONGOING)));
this._migrationStatusCardsContainer.addItem(
this._inProgressWarningMigrationButton.container,
{ flex: '0 0 auto' });
// successful
this._successfulMigrationButton = this._createStatusCard(
view,
IconPathHelper.completedMigration,
loc.MIGRATION_COMPLETED);
this.disposables.push(
this._successfulMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.SUCCEEDED)));
this._migrationStatusCardsContainer.addItem(
this._successfulMigrationButton.container,
{ flex: '0 0 auto' });
// completing
this._completingMigrationButton = this._createStatusCard(
view,
IconPathHelper.completingCutover,
loc.MIGRATION_CUTOVER_CARD);
this.disposables.push(
this._completingMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.COMPLETING)));
this._migrationStatusCardsContainer.addItem(
this._completingMigrationButton.container,
{ flex: '0 0 auto' });
// failed
this._failedMigrationButton = this._createStatusCard(
view,
IconPathHelper.error,
loc.MIGRATION_FAILED);
this.disposables.push(
this._failedMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.FAILED)));
this._migrationStatusCardsContainer.addItem(
this._failedMigrationButton.container,
{ flex: '0 0 auto' });
// all migrations
this._allMigrationButton = this._createStatusCard(
view,
IconPathHelper.view,
loc.VIEW_ALL);
this.disposables.push(
this._allMigrationButton.container.onDidClick(
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ALL)));
this._migrationStatusCardsContainer.addItem(
this._allMigrationButton.container,
{ flex: '0 0 auto' });
this._migrationStatusCardLoadingContainer = view.modelBuilder.loadingComponent()
.withItem(this._migrationStatusCardsContainer)
.component();
statusContainer.addItem(header, { CSSStyles: { 'margin-bottom': '10px' } });
statusContainer.addItem(this._selectServiceText, {});
statusContainer.addItem(this._migrationStatusCardLoadingContainer, {});
return statusContainer;
}
private async _createServiceSelector(view: azdata.ModelView): Promise<azdata.Component> {
const serviceContextLabel = await getSelectedServiceStatus();
this._serviceContextButton = view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.sqlMigrationService,
iconHeight: 22,
iconWidth: 22,
label: serviceContextLabel,
title: serviceContextLabel,
description: loc.MIGRATION_SERVICE_DESCRIPTION,
buttonType: azdata.ButtonType.Informational,
width: 375,
CSSStyles: { ...BUTTON_CSS },
})
.component();
this.disposables.push(
this._serviceContextButton.onDidClick(async () => {
const dialog = new SelectMigrationServiceDialog(async () => await this.onDialogClosed());
await dialog.initialize();
}));
return this._serviceContextButton;
}
private _updateStatusCard(
migrations: DatabaseMigration[],
card: StatusCard,
status: AdsMigrationStatus,
show?: boolean): void {
const list = filterMigrations(migrations, status);
const count = list?.length || 0;
card.container.display = count > 0 || show ? '' : 'none';
card.count.value = count.toString();
}
private async _updateSummaryStatus(): Promise<void> {
const serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
const isContextValid = isServiceContextValid(serviceContext);
await this._selectServiceText.updateCssStyles({ 'display': isContextValid ? 'none' : 'block' });
await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': isContextValid ? 'visible' : 'hidden' });
this._refreshButton.enabled = isContextValid;
}
}

View File

@@ -0,0 +1,205 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as loc from '../constants/strings';
import { getSqlServerName, getMigrationStatusImage } from '../api/utils';
import { logError, TelemetryViews } from '../telemtery';
import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
import { getResourceName } from '../api/azure';
import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
import { EmptySettingValue } from './tabBase';
import { DashboardStatusBar } from './sqlServerDashboard';
const MigrationDetailsBlobContainerTabId = 'MigrationDetailsBlobContainerTab';
export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase<MigrationDetailsBlobContainerTab> {
private _sourceDatabaseInfoField!: InfoFieldSchema;
private _sourceDetailsInfoField!: InfoFieldSchema;
private _sourceVersionInfoField!: InfoFieldSchema;
private _targetDatabaseInfoField!: InfoFieldSchema;
private _targetServerInfoField!: InfoFieldSchema;
private _targetVersionInfoField!: InfoFieldSchema;
private _migrationStatusInfoField!: InfoFieldSchema;
private _backupLocationInfoField!: InfoFieldSchema;
private _lastAppliedBackupInfoField!: InfoFieldSchema;
private _currentRestoringFileInfoField!: InfoFieldSchema;
constructor() {
super();
this.id = MigrationDetailsBlobContainerTabId;
}
public async create(
context: vscode.ExtensionContext,
view: azdata.ModelView,
onClosedCallback: () => Promise<void>,
statusBar: DashboardStatusBar,
): Promise<MigrationDetailsBlobContainerTab> {
this.view = view;
this.context = context;
this.onClosedCallback = onClosedCallback;
this.statusBar = statusBar;
await this.initialize(this.view);
return this;
}
public async refresh(): Promise<void> {
if (this.isRefreshing || this.model?.migration === undefined) {
return;
}
this.isRefreshing = true;
this.refreshButton.enabled = false;
this.refreshLoader.loading = true;
await this.statusBar.clearError();
try {
await this.model.fetchStatus();
} catch (e) {
await this.statusBar.showError(
loc.MIGRATION_STATUS_REFRESH_ERROR,
loc.MIGRATION_STATUS_REFRESH_ERROR,
e.message);
}
const migration = this.model?.migration;
await this.cutoverButton.updateCssStyles(
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
await this.showMigrationErrors(migration);
const sqlServerName = migration.properties.sourceServerName;
const sourceDatabaseName = migration.properties.sourceDatabaseName;
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
const targetDatabaseName = migration.name;
const targetServerName = getResourceName(migration.properties.scope);
const targetType = getMigrationTargetTypeEnum(migration);
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
this.databaseLabel.value = sourceDatabaseName;
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
this._sourceDetailsInfoField.text.value = sqlServerName;
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
this._targetDatabaseInfoField.text.value = targetDatabaseName;
this._targetServerInfoField.text.value = targetServerName;
this._targetVersionInfoField.text.value = targetServerVersion;
this._migrationStatusInfoField.text.value = getMigrationStatus(migration) ?? EmptySettingValue;
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
const storageAccountResourceId = migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId;
const blobContainerName
= migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName
?? migration.properties.migrationStatusDetails?.blobContainerName;
const backupLocation = storageAccountResourceId && blobContainerName
? `${storageAccountResourceId?.split('/').pop()} - ${blobContainerName}`
: blobContainerName;
this._backupLocationInfoField.text.value = backupLocation ?? EmptySettingValue;
this._lastAppliedBackupInfoField.text.value = migration.properties.migrationStatusDetails?.lastRestoredFilename ?? EmptySettingValue;
this._currentRestoringFileInfoField.text.value = this.getMigrationCurrentlyRestoringFile(migration) ?? EmptySettingValue;
this.cutoverButton.enabled = canCutoverMigration(migration);
this.cancelButton.enabled = canCancelMigration(migration);
this.retryButton.enabled = canRetryMigration(migration);
this.isRefreshing = false;
this.refreshLoader.loading = false;
this.refreshButton.enabled = true;
}
protected async initialize(view: azdata.ModelView): Promise<void> {
try {
const formItems: azdata.FormComponent<azdata.Component>[] = [
{ component: this.createMigrationToolbarContainer() },
{ component: await this.migrationInfoGrid() },
{
component: this.view.modelBuilder.separator()
.withProps({ width: '100%', CSSStyles: { 'padding': '0' } })
.component()
},
];
this.content = this.view.modelBuilder.formContainer()
.withFormItems(
formItems,
{ horizontal: false })
.withLayout({ width: '100%', padding: '0 0 0 15px' })
.withProps({ width: '100%', CSSStyles: { padding: '0 0 0 15px' } })
.component();
} catch (e) {
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
}
}
protected override async migrationInfoGrid(): Promise<azdata.FlexContainer> {
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
container.addItem(
infoField.flexContainer,
{ CSSStyles: { width: infoFieldWidth } });
};
const flexServer = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
const flexTarget = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
const flexStatus = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
addInfoFieldToContainer(this._backupLocationInfoField, flexStatus);
const flexFile = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', false);
addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
addInfoFieldToContainer(this._currentRestoringFileInfoField, flexFile);
const flexInfoProps = {
flex: '0',
CSSStyles: { 'flex': '0', 'width': infoFieldWidth }
};
const flexInfo = this.view.modelBuilder.flexContainer()
.withLayout({ flexWrap: 'wrap' })
.withProps({ width: '100%' })
.component();
flexInfo.addItem(flexServer, flexInfoProps);
flexInfo.addItem(flexTarget, flexInfoProps);
flexInfo.addItem(flexStatus, flexInfoProps);
flexInfo.addItem(flexFile, flexInfoProps);
return flexInfo;
}
}

View File

@@ -0,0 +1,389 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { IconPathHelper } from '../constants/iconPathHelper';
import * as loc from '../constants/strings';
import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage } from '../api/utils';
import { logError, TelemetryViews } from '../telemtery';
import * as styles from '../constants/styles';
import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
import { getResourceName } from '../api/azure';
import { EmptySettingValue } from './tabBase';
import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
import { DashboardStatusBar } from './sqlServerDashboard';
const MigrationDetailsFileShareTabId = 'MigrationDetailsFileShareTab';
interface ActiveBackupFileSchema {
fileName: string,
type: string,
status: string,
dataUploaded: string,
copyThroughput: string,
backupStartTime: string,
firstLSN: string,
lastLSN: string
}
export class MigrationDetailsFileShareTab extends MigrationDetailsTabBase<MigrationDetailsFileShareTab> {
private _sourceDatabaseInfoField!: InfoFieldSchema;
private _sourceDetailsInfoField!: InfoFieldSchema;
private _sourceVersionInfoField!: InfoFieldSchema;
private _targetDatabaseInfoField!: InfoFieldSchema;
private _targetServerInfoField!: InfoFieldSchema;
private _targetVersionInfoField!: InfoFieldSchema;
private _migrationStatusInfoField!: InfoFieldSchema;
private _fullBackupFileOnInfoField!: InfoFieldSchema;
private _backupLocationInfoField!: InfoFieldSchema;
private _lastLSNInfoField!: InfoFieldSchema;
private _lastAppliedBackupInfoField!: InfoFieldSchema;
private _lastAppliedBackupTakenOnInfoField!: InfoFieldSchema;
private _currentRestoringFileInfoField!: InfoFieldSchema;
private _fileCount!: azdata.TextComponent;
private _fileTable!: azdata.TableComponent;
private _emptyTableFill!: azdata.FlexContainer;
constructor() {
super();
this.id = MigrationDetailsFileShareTabId;
}
public async create(
context: vscode.ExtensionContext,
view: azdata.ModelView,
onClosedCallback: () => Promise<void>,
statusBar: DashboardStatusBar): Promise<MigrationDetailsFileShareTab> {
this.view = view;
this.context = context;
this.onClosedCallback = onClosedCallback;
this.statusBar = statusBar;
await this.initialize(this.view);
return this;
}
public async refresh(): Promise<void> {
if (this.isRefreshing || this.model?.migration === undefined) {
return;
}
this.isRefreshing = true;
this.refreshButton.enabled = false;
this.refreshLoader.loading = true;
await this.statusBar.clearError();
await this._fileTable.updateProperty('data', []);
try {
await this.model.fetchStatus();
} catch (e) {
await this.statusBar.showError(
loc.MIGRATION_STATUS_REFRESH_ERROR,
loc.MIGRATION_STATUS_REFRESH_ERROR,
e.message);
}
const migration = this.model?.migration;
await this.cutoverButton.updateCssStyles(
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
await this.showMigrationErrors(migration);
const sqlServerName = migration.properties.sourceServerName;
const sourceDatabaseName = migration.properties.sourceDatabaseName;
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
const targetDatabaseName = migration.name;
const targetServerName = getResourceName(migration.properties.scope);
const targetType = getMigrationTargetTypeEnum(migration);
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
let lastAppliedSSN: string;
let lastAppliedBackupFileTakenOn: string;
const tableData: ActiveBackupFileSchema[] = [];
migration.properties.migrationStatusDetails?.activeBackupSets?.forEach(
(activeBackupSet) => {
tableData.push(
...activeBackupSet.listOfBackupFiles.map(f => {
return {
fileName: f.fileName,
type: activeBackupSet.backupType,
status: f.status,
dataUploaded: `${convertByteSizeToReadableUnit(f.dataWritten)} / ${convertByteSizeToReadableUnit(f.totalSize)}`,
copyThroughput: (f.copyThroughput) ? (f.copyThroughput / 1024).toFixed(2) : EmptySettingValue,
backupStartTime: activeBackupSet.backupStartDate,
firstLSN: activeBackupSet.firstLSN,
lastLSN: activeBackupSet.lastLSN
};
})
);
if (activeBackupSet.listOfBackupFiles[0].fileName === migration.properties.migrationStatusDetails?.lastRestoredFilename) {
lastAppliedSSN = activeBackupSet.lastLSN;
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
}
});
this.databaseLabel.value = sourceDatabaseName;
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
this._sourceDetailsInfoField.text.value = sqlServerName;
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
this._targetDatabaseInfoField.text.value = targetDatabaseName;
this._targetServerInfoField.text.value = targetServerName;
this._targetVersionInfoField.text.value = targetServerVersion;
this._migrationStatusInfoField.text.value = getMigrationStatus(migration) ?? EmptySettingValue;
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
this._fullBackupFileOnInfoField.text.value = migration?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? EmptySettingValue;
const fileShare = migration.properties.backupConfiguration?.sourceLocation?.fileShare;
const backupLocation = fileShare?.path! ?? EmptySettingValue;
this._backupLocationInfoField.text.value = backupLocation ?? EmptySettingValue;
this._lastLSNInfoField.text.value = lastAppliedSSN! ?? EmptySettingValue;
this._lastAppliedBackupInfoField.text.value = migration.properties.migrationStatusDetails?.lastRestoredFilename ?? EmptySettingValue;
this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : EmptySettingValue;
this._currentRestoringFileInfoField.text.value = this.getMigrationCurrentlyRestoringFile(migration) ?? EmptySettingValue;
await this._fileCount.updateCssStyles({ ...styles.SECTION_HEADER_CSS, display: 'inline' });
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
if (tableData.length === 0) {
await this._emptyTableFill.updateCssStyles({ 'display': 'flex' });
this._fileTable.height = '50px';
await this._fileTable.updateProperty('data', []);
} else {
await this._emptyTableFill.updateCssStyles({ 'display': 'none' });
this._fileTable.height = '300px';
// Sorting files in descending order of backupStartTime
tableData.sort((file1, file2) => new Date(file1.backupStartTime) > new Date(file2.backupStartTime) ? - 1 : 1);
}
const data = tableData.map(row => [
row.fileName,
row.type,
row.status,
row.dataUploaded,
row.copyThroughput,
convertIsoTimeToLocalTime(row.backupStartTime).toLocaleString(),
row.firstLSN,
row.lastLSN
]) || [];
await this._fileTable.updateProperty('data', data);
this.cutoverButton.enabled = canCutoverMigration(migration);
this.cancelButton.enabled = canCancelMigration(migration);
this.retryButton.enabled = canRetryMigration(migration);
this.isRefreshing = false;
this.refreshLoader.loading = false;
this.refreshButton.enabled = true;
}
protected async initialize(view: azdata.ModelView): Promise<void> {
try {
this._fileCount = this.view.modelBuilder.text()
.withProps({
width: '500px',
CSSStyles: { ...styles.BODY_CSS }
}).component();
this._fileTable = this.view.modelBuilder.table()
.withProps({
ariaLabel: loc.ACTIVE_BACKUP_FILES,
CSSStyles: { 'padding-left': '0px', 'max-width': '1020px' },
data: [],
height: '300px',
columns: [
{
value: 'files',
name: loc.ACTIVE_BACKUP_FILES,
type: azdata.ColumnType.text,
width: 230,
},
{
value: 'type',
name: loc.TYPE,
width: 90,
type: azdata.ColumnType.text,
},
{
value: 'status',
name: loc.STATUS,
width: 60,
type: azdata.ColumnType.text,
},
{
value: 'uploaded',
name: loc.DATA_UPLOADED,
width: 120,
type: azdata.ColumnType.text,
},
{
value: 'throughput',
name: loc.COPY_THROUGHPUT,
width: 150,
type: azdata.ColumnType.text,
},
{
value: 'starttime',
name: loc.BACKUP_START_TIME,
width: 130,
type: azdata.ColumnType.text,
},
{
value: 'firstlsn',
name: loc.FIRST_LSN,
width: 120,
type: azdata.ColumnType.text,
},
{
value: 'lastlsn',
name: loc.LAST_LSN,
width: 120,
type: azdata.ColumnType.text,
}
],
}).component();
const emptyTableImage = this.view.modelBuilder.image()
.withProps({
iconPath: IconPathHelper.emptyTable,
iconHeight: '100px',
iconWidth: '100px',
height: '100px',
width: '100px',
CSSStyles: { 'text-align': 'center' }
}).component();
const emptyTableText = this.view.modelBuilder.text()
.withProps({
value: loc.EMPTY_TABLE_TEXT,
CSSStyles: {
...styles.NOTE_CSS,
'margin-top': '8px',
'text-align': 'center',
'width': '300px'
}
}).component();
this._emptyTableFill = this.view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column',
alignItems: 'center'
}).withItems([
emptyTableImage,
emptyTableText,
]).withProps({
width: '100%',
display: 'none'
}).component();
const formItems: azdata.FormComponent<azdata.Component>[] = [
{ component: this.createMigrationToolbarContainer() },
{ component: await this.migrationInfoGrid() },
{
component: this.view.modelBuilder.separator()
.withProps({ width: '100%', CSSStyles: { 'padding': '0' } })
.component()
},
{ component: this._fileCount },
{ component: this._fileTable },
{ component: this._emptyTableFill }
];
const formContainer = this.view.modelBuilder.formContainer()
.withFormItems(
formItems,
{ horizontal: false })
.withLayout({ width: '100%', padding: '0 0 0 15px' })
.withProps({ width: '100%', CSSStyles: { padding: '0 0 0 15px' } })
.component();
this.content = formContainer;
} catch (e) {
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
}
}
protected async migrationInfoGrid(): Promise<azdata.FlexContainer> {
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
container.addItem(
infoField.flexContainer,
{ CSSStyles: { width: infoFieldWidth } });
};
const flexServer = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
const flexTarget = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
const flexStatus = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', false);
this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
addInfoFieldToContainer(this._fullBackupFileOnInfoField, flexStatus);
addInfoFieldToContainer(this._backupLocationInfoField, flexStatus);
const flexFile = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._lastLSNInfoField = await this.createInfoField(loc.LAST_APPLIED_LSN, '', false);
this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', false);
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', false);
addInfoFieldToContainer(this._lastLSNInfoField, flexFile);
addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
addInfoFieldToContainer(this._lastAppliedBackupTakenOnInfoField, flexFile);
addInfoFieldToContainer(this._currentRestoringFileInfoField, flexFile);
const flexInfoProps = {
flex: '0',
CSSStyles: {
'flex': '0',
'width': infoFieldWidth
}
};
const flexInfo = this.view.modelBuilder.flexContainer()
.withLayout({ flexWrap: 'wrap' })
.withProps({ width: '100%' })
.component();
flexInfo.addItem(flexServer, flexInfoProps);
flexInfo.addItem(flexTarget, flexInfoProps);
flexInfo.addItem(flexStatus, flexInfoProps);
flexInfo.addItem(flexFile, flexInfoProps);
return flexInfo;
}
}

View File

@@ -0,0 +1,463 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { IconPathHelper } from '../constants/iconPathHelper';
import { MigrationServiceContext } from '../models/migrationLocalStorage';
import * as loc from '../constants/strings';
import * as styles from '../constants/styles';
import { DatabaseMigration } from '../api/azure';
import { TabBase } from './tabBase';
import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migrationCutoverDialogModel';
import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog';
import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
import { MigrationTargetType } from '../models/stateMachine';
import { DashboardStatusBar } from './sqlServerDashboard';
export const infoFieldLgWidth: string = '330px';
export const infoFieldWidth: string = '250px';
const statusImageSize: number = 14;
export const MigrationTargetTypeName: loc.LookupTable<string> = {
[MigrationTargetType.SQLMI]: loc.AZURE_SQL_DATABASE_MANAGED_INSTANCE,
[MigrationTargetType.SQLVM]: loc.AZURE_SQL_DATABASE_VIRTUAL_MACHINE,
[MigrationTargetType.SQLDB]: loc.AZURE_SQL_DATABASE,
};
export interface InfoFieldSchema {
flexContainer: azdata.FlexContainer,
text: azdata.TextComponent,
icon?: azdata.ImageComponent,
}
export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
protected model!: MigrationCutoverDialogModel;
protected databaseLabel!: azdata.TextComponent;
protected serviceContext!: MigrationServiceContext;
protected onClosedCallback!: () => Promise<void>;
protected cutoverButton!: azdata.ButtonComponent;
protected refreshButton!: azdata.ButtonComponent;
protected cancelButton!: azdata.ButtonComponent;
protected refreshLoader!: azdata.LoadingComponent;
protected copyDatabaseMigrationDetails!: azdata.ButtonComponent;
protected newSupportRequest!: azdata.ButtonComponent;
protected retryButton!: azdata.ButtonComponent;
protected summaryTextComponent: azdata.TextComponent[] = [];
public abstract create(context: vscode.ExtensionContext, view: azdata.ModelView, onClosedCallback: () => Promise<void>, statusBar: DashboardStatusBar): Promise<T>;
protected abstract migrationInfoGrid(): Promise<azdata.FlexContainer>;
constructor() {
super();
this.title = '';
}
public async setMigrationContext(
serviceContext: MigrationServiceContext,
migration: DatabaseMigration): Promise<void> {
this.serviceContext = serviceContext;
this.model = new MigrationCutoverDialogModel(serviceContext, migration);
await this.refresh();
}
protected createBreadcrumbContainer(): azdata.FlexContainer {
const migrationsTabLink = this.view.modelBuilder.hyperlink()
.withProps({
label: loc.BREADCRUMB_MIGRATIONS,
url: '',
title: loc.BREADCRUMB_MIGRATIONS,
CSSStyles: {
'padding': '5px 5px 5px 0',
'font-size': '13px'
}
})
.component();
this.disposables.push(
migrationsTabLink.onDidClick(
async (e) => await this.onClosedCallback()));
const breadCrumbImage = this.view.modelBuilder.image()
.withProps({
iconPath: IconPathHelper.breadCrumb,
iconHeight: 8,
iconWidth: 8,
width: 8,
height: 8,
CSSStyles: { 'padding': '4px' }
}).component();
this.databaseLabel = this.view.modelBuilder.text()
.withProps({
textType: azdata.TextType.Normal,
value: '...',
CSSStyles: {
'font-size': '16px',
'font-weight': '600',
'margin-block-start': '0',
'margin-block-end': '0',
}
}).component();
return this.view.modelBuilder.flexContainer()
.withItems(
[migrationsTabLink, breadCrumbImage, this.databaseLabel],
{ flex: '0 0 auto' })
.withLayout({
flexFlow: 'row',
alignItems: 'center',
alignContent: 'center',
})
.withProps({
height: 20,
CSSStyles: { 'padding': '0', 'margin-bottom': '5px' }
})
.component();
}
protected createMigrationToolbarContainer(): azdata.FlexContainer {
const toolbarContainer = this.view.modelBuilder.toolbarContainer();
const buttonHeight = 20;
this.cutoverButton = this.view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.cutover,
iconHeight: '16px',
iconWidth: '16px',
label: loc.COMPLETE_CUTOVER,
height: buttonHeight,
enabled: false,
CSSStyles: { 'display': 'none' }
}).component();
this.disposables.push(
this.cutoverButton.onDidClick(async (e) => {
await this.statusBar.clearError();
await this.refresh();
const dialog = new ConfirmCutoverDialog(this.model);
await dialog.initialize();
if (this.model.CutoverError) {
await this.statusBar.showError(
loc.MIGRATION_CUTOVER_ERROR,
loc.MIGRATION_CUTOVER_ERROR,
this.model.CutoverError.message);
}
}));
this.cancelButton = this.view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.cancel,
iconHeight: '16px',
iconWidth: '16px',
label: loc.CANCEL_MIGRATION,
height: buttonHeight,
enabled: false,
}).component();
this.disposables.push(
this.cancelButton.onDidClick((e) => {
void vscode.window.showInformationMessage(
loc.CANCEL_MIGRATION_CONFIRMATION,
{ modal: true },
loc.YES,
loc.NO
).then(async (v) => {
if (v === loc.YES) {
await this.statusBar.clearError();
await this.model.cancelMigration();
await this.refresh();
if (this.model.CancelMigrationError) {
{
await this.statusBar.showError(
loc.MIGRATION_CANCELLATION_ERROR,
loc.MIGRATION_CANCELLATION_ERROR,
this.model.CancelMigrationError.message);
}
}
}
});
}));
this.retryButton = this.view.modelBuilder.button()
.withProps({
label: loc.RETRY_MIGRATION,
iconPath: IconPathHelper.retry,
enabled: false,
iconHeight: '16px',
iconWidth: '16px',
height: buttonHeight,
}).component();
this.disposables.push(
this.retryButton.onDidClick(
async (e) => {
await this.refresh();
const retryMigrationDialog = new RetryMigrationDialog(
this.context,
this.serviceContext,
this.model.migration,
this.onClosedCallback);
await retryMigrationDialog.openDialog();
}
));
this.copyDatabaseMigrationDetails = this.view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.copy,
iconHeight: '16px',
iconWidth: '16px',
label: loc.COPY_MIGRATION_DETAILS,
height: buttonHeight,
}).component();
this.disposables.push(
this.copyDatabaseMigrationDetails.onDidClick(async (e) => {
await this.refresh();
await vscode.env.clipboard.writeText(this._getMigrationDetails());
void vscode.window.showInformationMessage(loc.DETAILS_COPIED);
}));
this.newSupportRequest = this.view.modelBuilder.button()
.withProps({
label: loc.NEW_SUPPORT_REQUEST,
iconPath: IconPathHelper.newSupportRequest,
iconHeight: '16px',
iconWidth: '16px',
height: buttonHeight,
}).component();
this.disposables.push(
this.newSupportRequest.onDidClick(async (e) => {
const serviceId = this.model.migration.properties.migrationService;
const supportUrl = `https://portal.azure.com/#resource${serviceId}/supportrequest`;
await vscode.env.openExternal(vscode.Uri.parse(supportUrl));
}));
this.refreshButton = this.view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.refresh,
iconHeight: '16px',
iconWidth: '16px',
label: loc.REFRESH_BUTTON_TEXT,
height: buttonHeight,
}).component();
this.disposables.push(
this.refreshButton.onDidClick(
async (e) => await this.refresh()));
this.refreshLoader = this.view.modelBuilder.loadingComponent()
.withProps({
loading: false,
CSSStyles: {
'height': '8px',
'margin-top': '4px'
}
}).component();
toolbarContainer.addToolbarItems([
<azdata.ToolbarComponent>{ component: this.cutoverButton },
<azdata.ToolbarComponent>{ component: this.cancelButton },
<azdata.ToolbarComponent>{ component: this.retryButton },
<azdata.ToolbarComponent>{ component: this.copyDatabaseMigrationDetails, toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this.newSupportRequest, toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this.refreshButton },
<azdata.ToolbarComponent>{ component: this.refreshLoader },
]);
return this.view.modelBuilder.flexContainer()
.withItems([
this.createBreadcrumbContainer(),
toolbarContainer.component(),
])
.withLayout({ flexFlow: 'column', width: '100%' })
.component();
}
protected async createInfoCard(
label: string,
iconPath: azdata.IconPath
): Promise<azdata.FlexContainer> {
const defaultValue = (0).toLocaleString();
const flexContainer = this.view.modelBuilder.flexContainer()
.withProps({
width: 168,
CSSStyles: {
'flex-direction': 'column',
'margin': '0 12px 0 0',
'box-sizing': 'border-box',
'border': '1px solid rgba(204, 204, 204, 0.5)',
'box-shadow': '0px 2px 4px rgba(0, 0, 0, 0.1)',
'border-radius': '2px',
}
}).component();
const labelComponent = this.view.modelBuilder.text()
.withProps({
value: label,
CSSStyles: {
'font-size': '13px',
'font-weight': '600',
'margin': '5px',
}
}).component();
flexContainer.addItem(labelComponent);
const iconComponent = this.view.modelBuilder.image()
.withProps({
iconPath: iconPath,
iconHeight: 16,
iconWidth: 16,
height: 16,
width: 16,
CSSStyles: {
'margin': '5px 5px 5px 5px',
'padding': '0'
}
}).component();
const textComponent = this.view.modelBuilder.text()
.withProps({
value: defaultValue,
title: defaultValue,
CSSStyles: {
'font-size': '20px',
'font-weight': '600',
'margin': '0 5px 0 5px'
}
}).component();
this.summaryTextComponent.push(textComponent);
const iconTextComponent = this.view.modelBuilder.flexContainer()
.withItems([iconComponent, textComponent])
.withLayout({ alignItems: 'center' })
.withProps({
CSSStyles: {
'flex-direction': 'row',
'margin': '0 0 0 5px',
'padding': '0',
},
display: 'inline-flex'
}).component();
flexContainer.addItem(iconTextComponent);
return flexContainer;
}
protected async createInfoField(label: string, value: string, defaultHidden: boolean = false, iconPath?: azdata.IconPath): Promise<{
flexContainer: azdata.FlexContainer,
text: azdata.TextComponent,
icon?: azdata.ImageComponent
}> {
const flexContainer = this.view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
'flex-direction': 'column',
'padding-right': '12px'
}
}).component();
const labelComponent = this.view.modelBuilder.text()
.withProps({
value: label,
CSSStyles: {
...styles.LIGHT_LABEL_CSS,
'margin-bottom': '0',
}
}).component();
flexContainer.addItem(labelComponent);
const textComponent = this.view.modelBuilder.text()
.withProps({
value: value,
title: value,
description: value,
width: '100%',
CSSStyles: {
'font-size': '13px',
'line-height': '18px',
'margin': '4px 0 12px',
'overflow': 'hidden',
'text-overflow': 'ellipsis',
'max-width': '230px',
'display': 'inline-block',
}
}).component();
let iconComponent;
if (iconPath) {
iconComponent = this.view.modelBuilder.image()
.withProps({
iconPath: (iconPath === ' ') ? undefined : iconPath,
iconHeight: statusImageSize,
iconWidth: statusImageSize,
height: statusImageSize,
width: statusImageSize,
title: value,
CSSStyles: {
'margin': '7px 3px 0 0',
'padding': '0'
}
}).component();
const iconTextComponent = this.view.modelBuilder.flexContainer()
.withItems([
iconComponent,
textComponent
]).withProps({
CSSStyles: {
'margin': '0',
'padding': '0'
},
display: 'inline-flex'
}).component();
flexContainer.addItem(iconTextComponent);
} else {
flexContainer.addItem(textComponent);
}
return {
flexContainer: flexContainer,
text: textComponent,
icon: iconComponent
};
}
protected async showMigrationErrors(migration: DatabaseMigration): Promise<void> {
const errorMessage = this.getMigrationErrors(migration);
if (errorMessage?.length > 0) {
await this.statusBar.showError(
loc.MIGRATION_ERROR_DETAILS_TITLE,
loc.MIGRATION_ERROR_DETAILS_LABEL,
errorMessage);
}
}
protected getMigrationCurrentlyRestoringFile(migration: DatabaseMigration): string | undefined {
const lastAppliedBackupFile = this.getMigrationLastAppliedBackupFile(migration);
const currentRestoringFile = migration?.properties?.migrationStatusDetails?.currentRestoringFilename;
return currentRestoringFile === lastAppliedBackupFile
&& currentRestoringFile && currentRestoringFile.length > 0
? loc.ALL_BACKUPS_RESTORED
: currentRestoringFile;
}
protected getMigrationLastAppliedBackupFile(migration: DatabaseMigration): string | undefined {
return migration?.properties?.migrationStatusDetails?.lastRestoredFilename
|| migration?.properties?.offlineConfiguration?.lastBackupName;
}
private _getMigrationDetails(): string {
return JSON.stringify(this.model.migration, undefined, 2);
}
}

View File

@@ -0,0 +1,567 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as loc from '../constants/strings';
import { getSqlServerName, getMigrationStatusImage, getPipelineStatusImage, debounce } from '../api/utils';
import { logError, TelemetryViews } from '../telemtery';
import { canCancelMigration, canCutoverMigration, canRetryMigration, formatDateTimeString, formatNumber, formatSizeBytes, formatSizeKb, formatTime, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation, PipelineStatusCodes } from '../constants/helper';
import { CopyProgressDetail, getResourceName } from '../api/azure';
import { InfoFieldSchema, infoFieldLgWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
import { EmptySettingValue } from './tabBase';
import { IconPathHelper } from '../constants/iconPathHelper';
import { DashboardStatusBar } from './sqlServerDashboard';
import { EOL } from 'os';
const MigrationDetailsTableTabId = 'MigrationDetailsTableTab';
const TableColumns = {
tableName: 'tableName',
status: 'status',
dataRead: 'dataRead',
dataWritten: 'dataWritten',
rowsRead: 'rowsRead',
rowsCopied: 'rowsCopied',
copyThroughput: 'copyThroughput',
copyDuration: 'copyDuration',
parallelCopyType: 'parallelCopyType',
usedParallelCopies: 'usedParallelCopies',
copyStart: 'copyStart',
};
enum SummaryCardIndex {
TotalTables = 0,
InProgressTables = 1,
SuccessfulTables = 2,
FailedTables = 3,
CanceledTables = 4,
}
export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationDetailsTableTab> {
private _sourceDatabaseInfoField!: InfoFieldSchema;
private _sourceDetailsInfoField!: InfoFieldSchema;
private _sourceVersionInfoField!: InfoFieldSchema;
private _targetDatabaseInfoField!: InfoFieldSchema;
private _targetServerInfoField!: InfoFieldSchema;
private _targetVersionInfoField!: InfoFieldSchema;
private _migrationStatusInfoField!: InfoFieldSchema;
private _serverObjectsInfoField!: InfoFieldSchema;
private _tableFilterInputBox!: azdata.InputBoxComponent;
private _columnSortDropdown!: azdata.DropDownComponent;
private _columnSortCheckbox!: azdata.CheckBoxComponent;
private _progressTable!: azdata.TableComponent;
private _progressDetail: CopyProgressDetail[] = [];
constructor() {
super();
this.id = MigrationDetailsTableTabId;
}
public async create(
context: vscode.ExtensionContext,
view: azdata.ModelView,
onClosedCallback: () => Promise<void>,
statusBar: DashboardStatusBar): Promise<MigrationDetailsTableTab> {
this.view = view;
this.context = context;
this.onClosedCallback = onClosedCallback;
this.statusBar = statusBar;
await this.initialize(this.view);
return this;
}
@debounce(500)
public async refresh(): Promise<void> {
if (this.isRefreshing) {
return;
}
this.isRefreshing = true;
this.refreshButton.enabled = false;
this.refreshLoader.loading = true;
await this.statusBar.clearError();
try {
await this.model.fetchStatus();
await this._loadData();
} catch (e) {
await this.statusBar.showError(
loc.MIGRATION_STATUS_REFRESH_ERROR,
loc.MIGRATION_STATUS_REFRESH_ERROR,
e.message);
}
this.isRefreshing = false;
this.refreshLoader.loading = false;
this.refreshButton.enabled = true;
}
private async _loadData(): Promise<void> {
const migration = this.model?.migration;
await this.showMigrationErrors(this.model?.migration);
await this.cutoverButton.updateCssStyles(
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
const sqlServerName = migration?.properties.sourceServerName;
const sourceDatabaseName = migration?.properties.sourceDatabaseName;
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
const targetDatabaseName = migration?.name;
const targetServerName = getResourceName(migration?.properties.scope);
const targetType = getMigrationTargetTypeEnum(migration);
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
const hashSet: loc.LookupTable<number> = {};
this._progressDetail = migration?.properties.migrationStatusDetails?.listOfCopyProgressDetails ?? [];
await this._populateTableData(hashSet);
const successCount = hashSet[PipelineStatusCodes.Succeeded] ?? 0;
const cancelledCount =
(hashSet[PipelineStatusCodes.Canceled] ?? 0) +
(hashSet[PipelineStatusCodes.Cancelled] ?? 0);
const failedCount = hashSet[PipelineStatusCodes.Failed] ?? 0;
const inProgressCount =
(hashSet[PipelineStatusCodes.Queued] ?? 0) +
(hashSet[PipelineStatusCodes.CopyFinished] ?? 0) +
(hashSet[PipelineStatusCodes.Copying] ?? 0) +
(hashSet[PipelineStatusCodes.PreparingForCopy] ?? 0) +
(hashSet[PipelineStatusCodes.RebuildingIndexes] ?? 0) +
(hashSet[PipelineStatusCodes.InProgress] ?? 0);
const totalCount = migration.properties.migrationStatusDetails?.listOfCopyProgressDetails.length ?? 0;
this._updateSummaryComponent(SummaryCardIndex.TotalTables, totalCount);
this._updateSummaryComponent(SummaryCardIndex.InProgressTables, inProgressCount);
this._updateSummaryComponent(SummaryCardIndex.SuccessfulTables, successCount);
this._updateSummaryComponent(SummaryCardIndex.FailedTables, failedCount);
this._updateSummaryComponent(SummaryCardIndex.CanceledTables, cancelledCount);
this.databaseLabel.value = sourceDatabaseName;
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
this._sourceDetailsInfoField.text.value = sqlServerName;
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
this._targetDatabaseInfoField.text.value = targetDatabaseName;
this._targetServerInfoField.text.value = targetServerName;
this._targetVersionInfoField.text.value = targetServerVersion;
this._migrationStatusInfoField.text.value = getMigrationStatus(migration) ?? EmptySettingValue;
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
this._serverObjectsInfoField.text.value = totalCount.toLocaleString();
this.cutoverButton.enabled = canCutoverMigration(migration);
this.cancelButton.enabled = canCancelMigration(migration);
this.retryButton.enabled = canRetryMigration(migration);
}
private async _populateTableData(hashSet: loc.LookupTable<number> = {}): Promise<void> {
if (this._progressTable.data.length > 0) {
await this._progressTable.updateProperty('data', []);
}
// Sort table data
this._sortTableMigrations(
this._progressDetail,
(<azdata.CategoryValue>this._columnSortDropdown.value).name,
this._columnSortCheckbox.checked === true);
const data = this._progressDetail.map((d) => {
hashSet[d.status] = (hashSet[d.status] ?? 0) + 1;
return [
d.tableName,
<azdata.HyperlinkColumnCellValue>{
icon: getPipelineStatusImage(d.status),
title: loc.PipelineRunStatus[d.status] ?? d.status?.toUpperCase(),
},
formatSizeBytes(d.dataRead),
formatSizeBytes(d.dataWritten),
formatNumber(d.rowsRead),
formatNumber(d.rowsCopied),
formatSizeKb(d.copyThroughput),
formatTime((d.copyDuration ?? 0) * 1000),
loc.ParallelCopyType[d.parallelCopyType] ?? d.parallelCopyType,
d.usedParallelCopies,
formatDateTimeString(d.copyStart),
];
}) ?? [];
// Filter tableData
const filteredData = this._filterTables(data, this._tableFilterInputBox.value);
await this._progressTable.updateProperty('data', filteredData);
}
protected async initialize(view: azdata.ModelView): Promise<void> {
try {
this._progressTable = this.view.modelBuilder.table()
.withProps({
ariaLabel: loc.ACTIVE_BACKUP_FILES,
CSSStyles: {
'padding-left': '0px',
'max-width': '1111px'
},
data: [],
height: '300px',
columns: [
{
value: TableColumns.tableName,
name: loc.SQLDB_COL_TABLE_NAME,
type: azdata.ColumnType.text,
width: 170,
},
<azdata.HyperlinkColumn>{
name: loc.STATUS,
value: TableColumns.status,
width: 106,
type: azdata.ColumnType.hyperlink,
icon: IconPathHelper.inProgressMigration,
showText: true,
},
{
value: TableColumns.dataRead,
name: loc.SQLDB_COL_DATA_READ,
width: 64,
type: azdata.ColumnType.text,
},
{
value: TableColumns.dataWritten,
name: loc.SQLDB_COL_DATA_WRITTEN,
width: 77,
type: azdata.ColumnType.text,
},
{
value: TableColumns.rowsRead,
name: loc.SQLDB_COL_ROWS_READ,
width: 68,
type: azdata.ColumnType.text,
},
{
value: TableColumns.rowsCopied,
name: loc.SQLDB_COL_ROWS_COPIED,
width: 77,
type: azdata.ColumnType.text,
},
{
value: TableColumns.copyThroughput,
name: loc.SQLDB_COL_COPY_THROUGHPUT,
width: 102,
type: azdata.ColumnType.text,
},
{
value: TableColumns.copyDuration,
name: loc.SQLDB_COL_COPY_DURATION,
width: 87,
type: azdata.ColumnType.text,
},
{
value: TableColumns.parallelCopyType,
name: loc.SQLDB_COL_PARRALEL_COPY_TYPE,
width: 104,
type: azdata.ColumnType.text,
},
{
value: TableColumns.usedParallelCopies,
name: loc.SQLDB_COL_USED_PARALLEL_COPIES,
width: 116,
type: azdata.ColumnType.text,
},
{
value: TableColumns.copyStart,
name: loc.SQLDB_COL_COPY_START,
width: 140,
type: azdata.ColumnType.text,
},
],
}).component();
const formItems: azdata.FormComponent<azdata.Component>[] = [
{ component: this.createMigrationToolbarContainer() },
{ component: await this.migrationInfoGrid() },
{
component: this.view.modelBuilder.separator()
.withProps({ width: '100%', CSSStyles: { 'padding': '0' } })
.component()
},
{ component: await this._createStatusBar() },
{ component: await this._createTableFilter() },
{ component: this._progressTable },
];
this.disposables.push(
this._progressTable.onCellAction!(
async (rowState: azdata.ICellActionEventArgs) => {
const buttonState = <azdata.ICellActionEventArgs>rowState;
if (buttonState?.column === 1) {
const tableName = this._progressTable!.data[rowState.row][0] || null;
const tableProgress = this.model.migration.properties.migrationStatusDetails?.listOfCopyProgressDetails?.find(
progress => progress.tableName === tableName);
const errors = tableProgress?.errors || [];
const tableStatus = loc.PipelineRunStatus[tableProgress?.status ?? ''] ?? tableProgress?.status;
const statusMessage = loc.TABLE_MIGRATION_STATUS_LABEL(tableStatus);
const errorMessage = errors.join(EOL);
this.showDialogMessage(
loc.TABLE_MIGRATION_STATUS_TITLE,
statusMessage,
errorMessage);
}
}));
const formContainer = this.view.modelBuilder.formContainer()
.withFormItems(
formItems,
{ horizontal: false })
.withProps({ width: '100%', CSSStyles: { margin: '0 0 0 5px', padding: '0 15px 0 15px' } })
.component();
this.content = formContainer;
} catch (e) {
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
}
}
private _sortTableMigrations(data: CopyProgressDetail[], columnName: string, ascending: boolean): void {
const sortDir = ascending ? -1 : 1;
switch (columnName) {
case TableColumns.tableName:
data.sort((t1, t2) => this.stringCompare(t1.tableName, t2.tableName, sortDir));
return;
case TableColumns.status:
data.sort((t1, t2) => this.stringCompare(t1.status, t2.status, sortDir));
return;
case TableColumns.dataRead:
data.sort((t1, t2) => this.numberCompare(t1.dataRead, t2.dataRead, sortDir));
return;
case TableColumns.dataWritten:
data.sort((t1, t2) => this.numberCompare(t1.dataWritten, t2.dataWritten, sortDir));
return;
case TableColumns.rowsRead:
data.sort((t1, t2) => this.numberCompare(t1.rowsRead, t2.rowsRead, sortDir));
return;
case TableColumns.rowsCopied:
data.sort((t1, t2) => this.numberCompare(t1.rowsCopied, t2.rowsCopied, sortDir));
return;
case TableColumns.copyThroughput:
data.sort((t1, t2) => this.numberCompare(t1.copyThroughput, t2.copyThroughput, sortDir));
return;
case TableColumns.copyDuration:
data.sort((t1, t2) => this.numberCompare(t1.copyDuration, t2.copyDuration, sortDir));
return;
case TableColumns.parallelCopyType:
data.sort((t1, t2) => this.stringCompare(t1.parallelCopyType, t2.parallelCopyType, sortDir));
return;
case TableColumns.usedParallelCopies:
data.sort((t1, t2) => this.numberCompare(t1.usedParallelCopies, t2.usedParallelCopies, sortDir));
return;
case TableColumns.copyStart:
data.sort((t1, t2) => this.dateCompare(t1.copyStart, t2.copyStart, sortDir));
return;
}
}
private _updateSummaryComponent(cardIndex: number, value: number): void {
const stringValue = value.toLocaleString();
const textComponent = this.summaryTextComponent[cardIndex];
textComponent.value = stringValue;
textComponent.title = stringValue;
}
private _filterTables(tables: any[], value: string | undefined): any[] {
const lcValue = value?.toLowerCase() ?? '';
return lcValue.length > 0
? tables.filter((table: string[]) =>
table.some((col: string | { title: string }) => {
return typeof (col) === 'string'
? col.toLowerCase().includes(lcValue)
: col.title?.toLowerCase().includes(lcValue);
}))
: tables;
}
private async _createTableFilter(): Promise<azdata.FlexContainer> {
this._tableFilterInputBox = this.view.modelBuilder.inputBox()
.withProps({
inputType: 'text',
maxLength: 100,
width: 268,
placeHolder: loc.FILTER_SERVER_OBJECTS_PLACEHOLDER,
ariaLabel: loc.FILTER_SERVER_OBJECTS_ARIA_LABEL,
})
.component();
this.disposables.push(
this._tableFilterInputBox.onTextChanged(
async (value) => await this._populateTableData()));
const sortLabel = this.view.modelBuilder.text()
.withProps({
value: loc.SORT_LABEL,
CSSStyles: {
'font-size': '13px',
'font-weight': '600',
'margin': '3px 0 0 0',
},
}).component();
this._columnSortDropdown = this.view.modelBuilder.dropDown()
.withProps({
editable: false,
width: 150,
CSSStyles: { 'margin-left': '5px' },
value: <azdata.CategoryValue>{ name: TableColumns.copyStart, displayName: loc.START_TIME },
values: [
<azdata.CategoryValue>{ name: TableColumns.tableName, displayName: loc.SQLDB_COL_TABLE_NAME },
<azdata.CategoryValue>{ name: TableColumns.status, displayName: loc.STATUS },
<azdata.CategoryValue>{ name: TableColumns.dataRead, displayName: loc.SQLDB_COL_DATA_READ },
<azdata.CategoryValue>{ name: TableColumns.dataWritten, displayName: loc.SQLDB_COL_DATA_WRITTEN },
<azdata.CategoryValue>{ name: TableColumns.rowsRead, displayName: loc.SQLDB_COL_ROWS_READ },
<azdata.CategoryValue>{ name: TableColumns.rowsCopied, displayName: loc.SQLDB_COL_ROWS_COPIED },
<azdata.CategoryValue>{ name: TableColumns.copyThroughput, displayName: loc.SQLDB_COL_COPY_THROUGHPUT },
<azdata.CategoryValue>{ name: TableColumns.copyDuration, displayName: loc.SQLDB_COL_COPY_DURATION },
<azdata.CategoryValue>{ name: TableColumns.parallelCopyType, displayName: loc.SQLDB_COL_PARRALEL_COPY_TYPE },
<azdata.CategoryValue>{ name: TableColumns.usedParallelCopies, displayName: loc.SQLDB_COL_USED_PARALLEL_COPIES },
<azdata.CategoryValue>{ name: TableColumns.copyStart, displayName: loc.SQLDB_COL_COPY_START },
],
})
.component();
this.disposables.push(
this._columnSortDropdown.onValueChanged(
async (value) => await this._populateTableData()));
this._columnSortCheckbox = this.view.modelBuilder.checkBox()
.withProps({
label: loc.ASCENDING_LABEL,
checked: false,
CSSStyles: { 'margin-left': '15px' },
})
.component();
this.disposables.push(
this._columnSortCheckbox.onChanged(
async (value) => await this._populateTableData()));
const columnSortContainer = this.view.modelBuilder.flexContainer()
.withItems([sortLabel, this._columnSortDropdown])
.withProps({
CSSStyles: {
'justify-content': 'left',
'align-items': 'center',
'padding': '0px',
'display': 'flex',
'flex-direction': 'row',
},
}).component();
columnSortContainer.addItem(this._columnSortCheckbox, { flex: '0 0 auto' });
const flexContainer = this.view.modelBuilder.flexContainer()
.withProps({
width: '100%',
CSSStyles: {
'justify-content': 'left',
'align-items': 'center',
'padding': '0px',
'display': 'flex',
'flex-direction': 'row',
'flex-flow': 'wrap',
},
}).component();
flexContainer.addItem(this._tableFilterInputBox, { flex: '0' });
flexContainer.addItem(columnSortContainer, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
return flexContainer;
}
private async _createStatusBar(): Promise<azdata.FlexContainer> {
const serverObjectsLabel = this.view.modelBuilder.text()
.withProps({
value: loc.SERVER_OBJECTS_LABEL,
CSSStyles: {
'font-weight': '600',
'font-size': '14px',
'margin': '0 0 5px 0',
},
})
.component();
const flexContainer = this.view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'row',
flexWrap: 'wrap',
})
.component();
flexContainer.addItems([
await this.createInfoCard(loc.SERVER_OBJECTS_ALL_TABLES_LABEL, IconPathHelper.allTables),
await this.createInfoCard(loc.SERVER_OBJECTS_IN_PROGRESS_TABLES_LABEL, IconPathHelper.inProgressMigration),
await this.createInfoCard(loc.SERVER_OBJECTS_SUCCESSFUL_TABLES_LABEL, IconPathHelper.completedMigration),
await this.createInfoCard(loc.SERVER_OBJECTS_FAILED_TABLES_LABEL, IconPathHelper.error),
await this.createInfoCard(loc.SERVER_OBJECTS_CANCELLED_TABLES_LABEL, IconPathHelper.cancel)
], { flex: '0 0 auto', CSSStyles: { 'width': '168px' } });
return this.view.modelBuilder.flexContainer()
.withItems([serverObjectsLabel, flexContainer])
.withLayout({ flexFlow: 'column' })
.component();
}
protected async migrationInfoGrid(): Promise<azdata.FlexContainer> {
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
container.addItem(
infoField.flexContainer,
{ CSSStyles: { width: infoFieldLgWidth } });
};
const flexServer = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
const flexTarget = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
const flexStatus = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
this._serverObjectsInfoField = await this.createInfoField(loc.SERVER_OBJECTS_FIELD_LABEL, '');
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
addInfoFieldToContainer(this._serverObjectsInfoField, flexStatus);
const flexInfoProps = {
flex: '0',
CSSStyles: { 'flex': '0', 'width': infoFieldLgWidth }
};
const flexInfo = this.view.modelBuilder.flexContainer()
.withLayout({ flexWrap: 'wrap' })
.withProps({ width: '100%' })
.component();
flexInfo.addItem(flexServer, flexInfoProps);
flexInfo.addItem(flexTarget, flexInfoProps);
flexInfo.addItem(flexStatus, flexInfoProps);
return flexInfo;
}
}

View File

@@ -0,0 +1,771 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { IconPathHelper } from '../constants/iconPathHelper';
import { getCurrentMigrations, getSelectedServiceStatus, MigrationLocalStorage } from '../models/migrationLocalStorage';
import * as loc from '../constants/strings';
import { filterMigrations, getMigrationDuration, getMigrationStatusImage, getMigrationStatusWithErrors, getMigrationTime } from '../api/utils';
import { SqlMigrationServiceDetailsDialog } from '../dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog';
import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog';
import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migrationCutoverDialogModel';
import { getMigrationTargetType, getMigrationMode, canRetryMigration, getMigrationModeEnum, canCancelMigration, canCutoverMigration, getMigrationStatus } from '../constants/helper';
import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
import { DatabaseMigration, getResourceName } from '../api/azure';
import { logError, TelemetryViews } from '../telemtery';
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
import { AdsMigrationStatus, EmptySettingValue, MenuCommands, TabBase } from './tabBase';
import { DashboardStatusBar } from './sqlServerDashboard';
import { MigrationMode } from '../models/stateMachine';
export const MigrationsListTabId = 'MigrationsListTab';
const TableColumns = {
sourceDatabase: 'sourceDatabase',
sourceServer: 'sourceServer',
status: 'status',
mode: 'mode',
targetType: 'targetType',
targetDatabse: 'targetDatabase',
targetServer: 'TargetServer',
duration: 'duration',
startTime: 'startTime',
finishTime: 'finishTime',
};
export class MigrationsListTab extends TabBase<MigrationsListTab> {
private _searchBox!: azdata.InputBoxComponent;
private _refresh!: azdata.ButtonComponent;
private _serviceContextButton!: azdata.ButtonComponent;
private _statusDropdown!: azdata.DropDownComponent;
private _columnSortDropdown!: azdata.DropDownComponent;
private _columnSortCheckbox!: azdata.CheckBoxComponent;
private _statusTable!: azdata.TableComponent;
private _refreshLoader!: azdata.LoadingComponent;
private _filteredMigrations: DatabaseMigration[] = [];
private _openMigrationDetails!: (migration: DatabaseMigration) => Promise<void>;
private _migrations: DatabaseMigration[] = [];
constructor() {
super();
this.id = MigrationsListTabId;
}
public async create(
context: vscode.ExtensionContext,
view: azdata.ModelView,
openMigrationDetails: (migration: DatabaseMigration) => Promise<void>,
statusBar: DashboardStatusBar,
): Promise<MigrationsListTab> {
this.view = view;
this.context = context;
this._openMigrationDetails = openMigrationDetails;
this.statusBar = statusBar;
await this.initialize();
return this;
}
public onDialogClosed = async (): Promise<void> =>
await this.updateServiceContext(this._serviceContextButton);
public async setMigrationFilter(filter: AdsMigrationStatus): Promise<void> {
if (this._statusDropdown.values && this._statusDropdown.values.length > 0) {
const statusFilter = (<azdata.CategoryValue[]>this._statusDropdown.values)
.find(value => value.name === filter.toString());
this._statusDropdown.value = statusFilter;
}
}
public async refresh(): Promise<void> {
if (this.isRefreshing) {
return;
}
this.isRefreshing = true;
this._refresh.enabled = false;
this._refreshLoader.loading = true;
await this.statusBar.clearError();
try {
await this._statusTable.updateProperty('data', []);
this._migrations = await getCurrentMigrations();
await this._populateMigrationTable();
} catch (e) {
await this.statusBar.showError(
loc.DASHBOARD_REFRESH_MIGRATIONS_TITLE,
loc.DASHBOARD_REFRESH_MIGRATIONS_LABEL,
e.message);
logError(TelemetryViews.MigrationsTab, 'refreshMigrations', e);
} finally {
this._refreshLoader.loading = false;
this._refresh.enabled = true;
this.isRefreshing = false;
}
}
protected async initialize(): Promise<void> {
this._registerCommands();
this.content = this.view.modelBuilder.flexContainer()
.withItems(
[
this._createToolbar(),
await this._createSearchAndSortContainer(),
this._createStatusTable()
],
{ CSSStyles: { 'width': '100%' } }
).withLayout({
width: '100%',
flexFlow: 'column',
}).withProps({ CSSStyles: { 'padding': '0px' } })
.component();
}
private _createToolbar(): azdata.ToolbarContainer {
const toolbar = this.view.modelBuilder.toolbarContainer();
this._refresh = this.view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.refresh,
iconHeight: 24,
iconWidth: 24,
height: 24,
label: loc.REFRESH_BUTTON_LABEL,
}).component();
this.disposables.push(
this._refresh.onDidClick(
async (e) => await this.refresh()));
this._refreshLoader = this.view.modelBuilder.loadingComponent()
.withProps({
loading: false,
CSSStyles: {
'height': '8px',
'margin-top': '6px'
}
})
.component();
toolbar.addToolbarItems([
<azdata.ToolbarComponent>{ component: this.createNewMigrationButton(), toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this.createNewSupportRequestButton() },
<azdata.ToolbarComponent>{ component: this.createFeedbackButton(), toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this._refresh },
<azdata.ToolbarComponent>{ component: this._refreshLoader },
]);
return toolbar.component();
}
private async _createSearchAndSortContainer(): Promise<azdata.FlexContainer> {
const serviceContextLabel = await getSelectedServiceStatus();
this._serviceContextButton = this.view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.sqlMigrationService,
iconHeight: 22,
iconWidth: 22,
label: serviceContextLabel,
title: serviceContextLabel,
description: loc.MIGRATION_SERVICE_DESCRIPTION,
buttonType: azdata.ButtonType.Informational,
width: 230,
}).component();
const onDialogClosed = async (): Promise<void> =>
await this.updateServiceContext(this._serviceContextButton);
this.disposables.push(
this._serviceContextButton.onDidClick(
async () => {
const dialog = new SelectMigrationServiceDialog(onDialogClosed);
await dialog.initialize();
}));
this._searchBox = this.view.modelBuilder.inputBox()
.withProps({
stopEnterPropagation: true,
placeHolder: loc.SEARCH_FOR_MIGRATIONS,
width: '200px',
}).component();
this.disposables.push(
this._searchBox.onTextChanged(
async (value) => await this._populateMigrationTable()));
const searchLabel = this.view.modelBuilder.text()
.withProps({
value: loc.STATUS_LABEL,
CSSStyles: {
'font-size': '13px',
'font-weight': '600',
'margin': '3px 0 0 0',
},
}).component();
this._statusDropdown = this.view.modelBuilder.dropDown()
.withProps({
ariaLabel: loc.MIGRATION_STATUS_FILTER,
values: this._statusDropdownValues,
width: '150px'
}).component();
this.disposables.push(
this._statusDropdown.onValueChanged(
async (value) => await this._populateMigrationTable()));
const searchContainer = this.view.modelBuilder.flexContainer()
.withLayout({
alignContent: 'center',
alignItems: 'center',
}).withProps({ CSSStyles: { 'margin-left': '10px' } })
.component();
searchContainer.addItem(searchLabel, { flex: '0' });
searchContainer.addItem(this._statusDropdown, { flex: '0', CSSStyles: { 'margin-left': '5px' } });
const sortLabel = this.view.modelBuilder.text()
.withProps({
value: loc.SORT_LABEL,
CSSStyles: {
'font-size': '13px',
'font-weight': '600',
'margin': '3px 0 0 0',
},
}).component();
this._columnSortDropdown = this.view.modelBuilder.dropDown()
.withProps({
editable: false,
width: 120,
CSSStyles: { 'margin-left': '5px' },
value: <azdata.CategoryValue>{ name: TableColumns.startTime, displayName: loc.START_TIME },
values: [
<azdata.CategoryValue>{ name: TableColumns.sourceDatabase, displayName: loc.SRC_DATABASE },
<azdata.CategoryValue>{ name: TableColumns.sourceServer, displayName: loc.SRC_SERVER },
<azdata.CategoryValue>{ name: TableColumns.status, displayName: loc.STATUS_COLUMN },
<azdata.CategoryValue>{ name: TableColumns.mode, displayName: loc.MIGRATION_MODE },
<azdata.CategoryValue>{ name: TableColumns.targetType, displayName: loc.AZURE_SQL_TARGET },
<azdata.CategoryValue>{ name: TableColumns.targetDatabse, displayName: loc.TARGET_DATABASE_COLUMN },
<azdata.CategoryValue>{ name: TableColumns.targetServer, displayName: loc.TARGET_SERVER_COLUMN },
<azdata.CategoryValue>{ name: TableColumns.duration, displayName: loc.DURATION },
<azdata.CategoryValue>{ name: TableColumns.startTime, displayName: loc.START_TIME },
<azdata.CategoryValue>{ name: TableColumns.finishTime, displayName: loc.FINISH_TIME },
],
})
.component();
this.disposables.push(
this._columnSortDropdown.onValueChanged(
async (e) => await this._populateMigrationTable()));
this._columnSortCheckbox = this.view.modelBuilder.checkBox()
.withProps({
label: loc.ASCENDING_LABEL,
checked: false,
CSSStyles: { 'margin-left': '15px' },
})
.component();
this.disposables.push(
this._columnSortCheckbox.onChanged(
async (e) => await this._populateMigrationTable()));
const columnSortContainer = this.view.modelBuilder.flexContainer()
.withItems([sortLabel, this._columnSortDropdown])
.withProps({
CSSStyles: {
'justify-content': 'left',
'align-items': 'center',
'padding': '0px',
'display': 'flex',
'flex-direction': 'row',
},
}).component();
columnSortContainer.addItem(this._columnSortCheckbox, { flex: '0 0 auto' });
const flexContainer = this.view.modelBuilder.flexContainer()
.withProps({
width: '100%',
CSSStyles: {
'justify-content': 'left',
'align-items': 'center',
'padding': '0px',
'display': 'flex',
'flex-direction': 'row',
'flex-flow': 'wrap',
},
}).component();
flexContainer.addItem(this._serviceContextButton, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
flexContainer.addItem(this._searchBox, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
flexContainer.addItem(searchContainer, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
flexContainer.addItem(columnSortContainer, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
const container = this.view.modelBuilder.flexContainer()
.withProps({ width: '100%' })
.component();
container.addItem(flexContainer);
return container;
}
private _registerCommands(): void {
this.disposables.push(vscode.commands.registerCommand(
MenuCommands.Cutover,
async (migrationId: string) => {
try {
await this.statusBar.clearError();
const migration = this._migrations.find(
migration => migration.id === migrationId);
if (canRetryMigration(migration)) {
const cutoverDialogModel = new MigrationCutoverDialogModel(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
await cutoverDialogModel.fetchStatus();
const dialog = new ConfirmCutoverDialog(cutoverDialogModel);
await dialog.initialize();
if (cutoverDialogModel.CutoverError) {
void vscode.window.showErrorMessage(loc.MIGRATION_CUTOVER_ERROR);
logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, cutoverDialogModel.CutoverError);
}
} else {
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CUTOVER);
}
} catch (e) {
await this.statusBar.showError(
loc.MIGRATION_CUTOVER_ERROR,
loc.MIGRATION_CUTOVER_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, e);
}
}));
this.disposables.push(vscode.commands.registerCommand(
MenuCommands.ViewDatabase,
async (migrationId: string) => {
try {
await this.statusBar.clearError();
const migration = this._migrations.find(m => m.id === migrationId);
await this._openMigrationDetails(migration!);
} catch (e) {
await this.statusBar.showError(
loc.OPEN_MIGRATION_DETAILS_ERROR,
loc.OPEN_MIGRATION_DETAILS_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewDatabase, e);
}
}));
this.disposables.push(vscode.commands.registerCommand(
MenuCommands.ViewTarget,
async (migrationId: string) => {
try {
const migration = this._migrations.find(migration => migration.id === migrationId);
const url = 'https://portal.azure.com/#resource/' + migration!.properties.scope;
await vscode.env.openExternal(vscode.Uri.parse(url));
} catch (e) {
await this.statusBar.showError(
loc.OPEN_MIGRATION_TARGET_ERROR,
loc.OPEN_MIGRATION_TARGET_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewTarget, e);
}
}));
this.disposables.push(vscode.commands.registerCommand(
MenuCommands.ViewService,
async (migrationId: string) => {
try {
await this.statusBar.clearError();
const migration = this._migrations.find(migration => migration.id === migrationId);
const dialog = new SqlMigrationServiceDetailsDialog(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
await dialog.initialize();
} catch (e) {
await this.statusBar.showError(
loc.OPEN_MIGRATION_SERVICE_ERROR,
loc.OPEN_MIGRATION_SERVICE_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewService, e);
}
}));
this.disposables.push(vscode.commands.registerCommand(
MenuCommands.CopyMigration,
async (migrationId: string) => {
await this.statusBar.clearError();
const migration = this._migrations.find(migration => migration.id === migrationId);
const cutoverDialogModel = new MigrationCutoverDialogModel(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
try {
await cutoverDialogModel.fetchStatus();
} catch (e) {
await this.statusBar.showError(
loc.MIGRATION_STATUS_REFRESH_ERROR,
loc.MIGRATION_STATUS_REFRESH_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.CopyMigration, e);
}
await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migration, undefined, 2));
await vscode.window.showInformationMessage(loc.DETAILS_COPIED);
}));
this.disposables.push(vscode.commands.registerCommand(
MenuCommands.CancelMigration,
async (migrationId: string) => {
try {
await this.statusBar.clearError();
const migration = this._migrations.find(migration => migration.id === migrationId);
if (canCancelMigration(migration)) {
void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => {
if (v === loc.YES) {
const cutoverDialogModel = new MigrationCutoverDialogModel(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
await cutoverDialogModel.fetchStatus();
await cutoverDialogModel.cancelMigration();
if (cutoverDialogModel.CancelMigrationError) {
void vscode.window.showErrorMessage(loc.MIGRATION_CANNOT_CANCEL);
logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, cutoverDialogModel.CancelMigrationError);
}
}
});
} else {
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CANCEL);
}
} catch (e) {
await this.statusBar.showError(
loc.MIGRATION_CANCELLATION_ERROR,
loc.MIGRATION_CANCELLATION_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, e);
}
}));
this.disposables.push(vscode.commands.registerCommand(
MenuCommands.RetryMigration,
async (migrationId: string) => {
try {
await this.statusBar.clearError();
const migration = this._migrations.find(migration => migration.id === migrationId);
if (canRetryMigration(migration)) {
let retryMigrationDialog = new RetryMigrationDialog(
this.context,
await MigrationLocalStorage.getMigrationServiceContext(),
migration!,
async () => await this.onDialogClosed());
await retryMigrationDialog.openDialog();
}
else {
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_RETRY);
}
} catch (e) {
await this.statusBar.showError(
loc.MIGRATION_RETRY_ERROR,
loc.MIGRATION_RETRY_ERROR,
e.message);
logError(TelemetryViews.MigrationsTab, MenuCommands.RetryMigration, e);
}
}));
}
private _sortMigrations(migrations: DatabaseMigration[], columnName: string, ascending: boolean): void {
const sortDir = ascending ? -1 : 1;
switch (columnName) {
case TableColumns.sourceDatabase:
migrations.sort(
(m1, m2) => this.stringCompare(
m1.properties.sourceDatabaseName,
m2.properties.sourceDatabaseName,
sortDir));
return;
case TableColumns.sourceServer:
migrations.sort(
(m1, m2) => this.stringCompare(
m1.properties.sourceServerName,
m2.properties.sourceServerName,
sortDir));
return;
case TableColumns.status:
migrations.sort(
(m1, m2) => this.stringCompare(
getMigrationStatusWithErrors(m1),
getMigrationStatusWithErrors(m2),
sortDir));
return;
case TableColumns.mode:
migrations.sort(
(m1, m2) => this.stringCompare(
getMigrationMode(m1),
getMigrationMode(m2),
sortDir));
return;
case TableColumns.targetType:
migrations.sort(
(m1, m2) => this.stringCompare(
getMigrationTargetType(m1),
getMigrationTargetType(m2),
sortDir));
return;
case TableColumns.targetDatabse:
migrations.sort(
(m1, m2) => this.stringCompare(
getResourceName(m1.id),
getResourceName(m2.id),
sortDir));
return;
case TableColumns.targetServer:
migrations.sort(
(m1, m2) => this.stringCompare(
getResourceName(m1.properties.scope),
getResourceName(m2.properties.scope),
sortDir));
return;
case TableColumns.duration:
migrations.sort((m1, m2) => {
if (!m1.properties.startedOn) {
return sortDir;
} else if (!m2.properties.startedOn) {
return -sortDir;
}
const m1_startedOn = new Date(m1.properties.startedOn);
const m2_startedOn = new Date(m2.properties.startedOn);
const m1_endedOn = new Date(m1.properties.endedOn ?? Date.now());
const m2_endedOn = new Date(m2.properties.endedOn ?? Date.now());
const m1_duration = m1_endedOn.getTime() - m1_startedOn.getTime();
const m2_duration = m2_endedOn.getTime() - m2_startedOn.getTime();
return m1_duration > m2_duration ? -sortDir : sortDir;
});
return;
case TableColumns.startTime:
migrations.sort(
(m1, m2) => this.dateCompare(
m1.properties.startedOn,
m2.properties.startedOn,
sortDir));
return;
case TableColumns.finishTime:
migrations.sort(
(m1, m2) => this.dateCompare(
m1.properties.endedOn,
m2.properties.endedOn,
sortDir));
return;
}
}
private async _populateMigrationTable(): Promise<void> {
try {
this._filteredMigrations = filterMigrations(
this._migrations,
(<azdata.CategoryValue>this._statusDropdown.value).name,
this._searchBox.value!);
this._sortMigrations(
this._filteredMigrations,
(<azdata.CategoryValue>this._columnSortDropdown.value).name,
this._columnSortCheckbox.checked === true);
const data: any[] = this._filteredMigrations.map((migration, index) => {
return [
<azdata.HyperlinkColumnCellValue>{
icon: IconPathHelper.sqlDatabaseLogo,
title: migration.properties.sourceDatabaseName ?? EmptySettingValue,
}, // sourceDatabase
migration.properties.sourceServerName ?? EmptySettingValue, // sourceServer
<azdata.HyperlinkColumnCellValue>{
icon: getMigrationStatusImage(migration),
title: getMigrationStatusWithErrors(migration),
}, // statue
getMigrationMode(migration), // mode
getMigrationTargetType(migration), // targetType
getResourceName(migration.id), // targetDatabase
getResourceName(migration.properties.scope), // targetServer
getMigrationDuration(
migration.properties.startedOn,
migration.properties.endedOn), // duration
getMigrationTime(migration.properties.startedOn), // startTime
getMigrationTime(migration.properties.endedOn), // finishTime
<azdata.ContextMenuColumnCellValue>{
title: '',
context: migration.id,
commands: this._getMenuCommands(migration), // context menu
},
];
});
await this._statusTable.updateProperty('data', data);
} catch (e) {
await this.statusBar.showError(
loc.LOAD_MIGRATION_LIST_ERROR,
loc.LOAD_MIGRATION_LIST_ERROR,
e.message);
logError(TelemetryViews.MigrationStatusDialog, 'Error populating migrations list page', e);
}
}
private _createStatusTable(): azdata.TableComponent {
const headerCssStyles = undefined;
const rowCssStyles = undefined;
this._statusTable = this.view.modelBuilder.table()
.withProps({
ariaLabel: loc.MIGRATION_STATUS,
CSSStyles: { 'margin-left': '10px' },
data: [],
forceFitColumns: azdata.ColumnSizingMode.AutoFit,
height: '500px',
columns: [
<azdata.HyperlinkColumn>{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.SRC_DATABASE,
value: 'sourceDatabase',
width: 190,
type: azdata.ColumnType.hyperlink,
showText: true,
},
{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.SRC_SERVER,
value: 'sourceServer',
width: 190,
type: azdata.ColumnType.text,
},
<azdata.HyperlinkColumn>{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.STATUS_COLUMN,
value: 'status',
width: 120,
type: azdata.ColumnType.hyperlink,
},
{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.MIGRATION_MODE,
value: 'mode',
width: 55,
type: azdata.ColumnType.text,
},
{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.AZURE_SQL_TARGET,
value: 'targetType',
width: 120,
type: azdata.ColumnType.text,
},
{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.TARGET_DATABASE_COLUMN,
value: 'targetDatabase',
width: 125,
type: azdata.ColumnType.text,
},
{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.TARGET_SERVER_COLUMN,
value: 'targetServer',
width: 125,
type: azdata.ColumnType.text,
},
{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.DURATION,
value: 'duration',
width: 55,
type: azdata.ColumnType.text,
},
{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.START_TIME,
value: 'startTime',
width: 115,
type: azdata.ColumnType.text,
},
{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.FINISH_TIME,
value: 'finishTime',
width: 115,
type: azdata.ColumnType.text,
},
{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: '',
value: 'contextMenu',
width: 25,
type: azdata.ColumnType.contextMenu,
}
]
}).component();
this.disposables.push(this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => {
const buttonState = <azdata.ICellActionEventArgs>rowState;
const migration = this._filteredMigrations[rowState.row];
switch (buttonState?.column) {
case 2:
const status = getMigrationStatus(migration);
const statusMessage = loc.DATABASE_MIGRATION_STATUS_LABEL(status);
const errors = this.getMigrationErrors(migration!);
this.showDialogMessage(
loc.DATABASE_MIGRATION_STATUS_TITLE,
statusMessage,
errors);
break;
case 0:
await this._openMigrationDetails(migration);
break;
}
}));
return this._statusTable;
}
private _getMenuCommands(migration: DatabaseMigration): string[] {
const menuCommands: string[] = [];
if (getMigrationModeEnum(migration) === MigrationMode.ONLINE &&
canCutoverMigration(migration)) {
menuCommands.push(MenuCommands.Cutover);
}
menuCommands.push(...[
MenuCommands.ViewDatabase,
MenuCommands.ViewTarget,
MenuCommands.ViewService,
MenuCommands.CopyMigration]);
if (canCancelMigration(migration)) {
menuCommands.push(MenuCommands.CancelMigration);
}
return menuCommands;
}
private _statusDropdownValues: azdata.CategoryValue[] = [
{ displayName: loc.STATUS_ALL, name: AdsMigrationStatus.ALL },
{ displayName: loc.STATUS_ONGOING, name: AdsMigrationStatus.ONGOING },
{ displayName: loc.STATUS_COMPLETING, name: AdsMigrationStatus.COMPLETING },
{ displayName: loc.STATUS_SUCCEEDED, name: AdsMigrationStatus.SUCCEEDED },
{ displayName: loc.STATUS_FAILED, name: AdsMigrationStatus.FAILED }
];
}

View File

@@ -0,0 +1,148 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as loc from '../constants/strings';
import { AdsMigrationStatus, TabBase } from './tabBase';
import { MigrationsListTab, MigrationsListTabId } from './migrationsListTab';
import { DatabaseMigration } from '../api/azure';
import { MigrationLocalStorage } from '../models/migrationLocalStorage';
import { FileStorageType } from '../models/stateMachine';
import { MigrationDetailsTabBase } from './migrationDetailsTabBase';
import { MigrationDetailsFileShareTab } from './migrationDetailsFileShareTab';
import { MigrationDetailsBlobContainerTab } from './migrationDetailsBlobContainerTab';
import { MigrationDetailsTableTab } from './migrationDetailsTableTab';
import { DashboardStatusBar } from './sqlServerDashboard';
export const MigrationsTabId = 'MigrationsTab';
export class MigrationsTab extends TabBase<MigrationsTab> {
private _tab!: azdata.DivContainer;
private _migrationsListTab!: MigrationsListTab;
private _migrationDetailsTab!: MigrationDetailsTabBase<any>;
private _migrationDetailsFileShareTab!: MigrationDetailsTabBase<any>;
private _migrationDetailsBlobTab!: MigrationDetailsTabBase<any>;
private _migrationDetailsTableTab!: MigrationDetailsTabBase<any>;
private _selectedTabId: string | undefined = undefined;
constructor() {
super();
this.title = loc.DESKTOP_MIGRATIONS_TAB_TITLE;
this.id = MigrationsTabId;
}
public onDialogClosed = async (): Promise<void> =>
await this._migrationsListTab.onDialogClosed();
public async create(
context: vscode.ExtensionContext,
view: azdata.ModelView,
statusBar: DashboardStatusBar): Promise<MigrationsTab> {
this.context = context;
this.view = view;
this.statusBar = statusBar;
await this.initialize(view);
await this._openTab(this._migrationsListTab);
return this;
}
public async refresh(): Promise<void> {
switch (this._selectedTabId) {
case undefined:
case MigrationsListTabId:
return await this._migrationsListTab?.refresh();
default:
return await this._migrationDetailsTab?.refresh();
}
}
protected async initialize(view: azdata.ModelView): Promise<void> {
this._tab = this.view.modelBuilder.divContainer()
.withLayout({ height: '100%' })
.withProps({
CSSStyles: {
'margin': '0px',
'padding': '0px',
'width': '100%'
}
})
.component();
this._migrationsListTab = await new MigrationsListTab().create(
this.context,
this.view,
async (migration) => await this._openMigrationDetails(migration),
this.statusBar);
this.disposables.push(this._migrationsListTab);
this._migrationDetailsBlobTab = await new MigrationDetailsBlobContainerTab().create(
this.context,
this.view,
async () => await this._openMigrationsListTab(),
this.statusBar);
this.disposables.push(this._migrationDetailsBlobTab);
this._migrationDetailsFileShareTab = await new MigrationDetailsFileShareTab().create(
this.context,
this.view,
async () => await this._openMigrationsListTab(),
this.statusBar);
this.disposables.push(this._migrationDetailsFileShareTab);
this._migrationDetailsTableTab = await new MigrationDetailsTableTab().create(
this.context,
this.view,
async () => await this._openMigrationsListTab(),
this.statusBar);
this.disposables.push(this._migrationDetailsFileShareTab);
this.content = this._tab;
}
public async setMigrationFilter(filter: AdsMigrationStatus): Promise<void> {
await this._migrationsListTab?.setMigrationFilter(filter);
await this._openTab(this._migrationsListTab);
await this._migrationsListTab?.setMigrationFilter(filter);
}
private async _openMigrationDetails(migration: DatabaseMigration): Promise<void> {
switch (migration.properties.backupConfiguration?.sourceLocation?.fileStorageType) {
case FileStorageType.AzureBlob:
this._migrationDetailsTab = this._migrationDetailsBlobTab;
break;
case FileStorageType.FileShare:
this._migrationDetailsTab = this._migrationDetailsFileShareTab;
break;
case FileStorageType.None:
this._migrationDetailsTab = this._migrationDetailsTableTab;
break;
}
await this._migrationDetailsTab.setMigrationContext(
await MigrationLocalStorage.getMigrationServiceContext(),
migration);
await this._openTab(this._migrationDetailsTab);
}
private async _openMigrationsListTab(): Promise<void> {
await this.statusBar.clearError();
await this._openTab(this._migrationsListTab);
}
private async _openTab(tab: azdata.Tab): Promise<void> {
if (tab.id === this._selectedTabId) {
return;
}
this._tab.clearItems();
this._tab.addItem(tab.content);
this._selectedTabId = tab.id;
}
}

View File

@@ -5,793 +5,193 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { logError, TelemetryViews } from '../telemtery';
import * as loc from '../constants/strings';
import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
import { MigrationStatusDialog } from '../dialog/migrationStatus/migrationStatusDialog';
import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel';
import { filterMigrations } from '../api/utils';
import * as styles from '../constants/styles';
import * as nls from 'vscode-nls';
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
import { DatabaseMigration } from '../api/azure';
import { getCurrentMigrations, getSelectedServiceStatus, isServiceContextValid, MigrationLocalStorage } from '../models/migrationLocalStorage';
const localize = nls.loadMessageBundle();
import { DashboardTab } from './dashboardTab';
import { MigrationsTab, MigrationsTabId } from './migrationsTab';
import { AdsMigrationStatus } from './tabBase';
interface IActionMetadata {
title?: string,
description?: string,
link?: string,
iconPath?: azdata.ThemedIconPath,
command?: string;
export interface DashboardStatusBar {
showError: (errorTitle: string, errorLable: string, errorDescription: string) => Promise<void>;
clearError: () => Promise<void>;
errorTitle: string;
errorLabel: string;
errorDescription: string;
}
const maxWidth = 800;
const BUTTON_CSS = {
'font-size': '13px',
'line-height': '18px',
'margin': '4px 0',
'text-align': 'left',
};
interface StatusCard {
container: azdata.DivContainer;
count: azdata.TextComponent,
textContainer?: azdata.FlexContainer,
warningContainer?: azdata.FlexContainer,
warningText?: azdata.TextComponent,
}
export class DashboardWidget {
export class DashboardWidget implements DashboardStatusBar {
private _context: vscode.ExtensionContext;
private _migrationStatusCardsContainer!: azdata.FlexContainer;
private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent;
private _view!: azdata.ModelView;
private _inProgressMigrationButton!: StatusCard;
private _inProgressWarningMigrationButton!: StatusCard;
private _allMigrationButton!: StatusCard;
private _successfulMigrationButton!: StatusCard;
private _failedMigrationButton!: StatusCard;
private _completingMigrationButton!: StatusCard;
private _selectServiceText!: azdata.TextComponent;
private _serviceContextButton!: azdata.ButtonComponent;
private _refreshButton!: azdata.ButtonComponent;
private _tabs!: azdata.TabbedPanelComponent;
private _statusInfoBox!: azdata.InfoBoxComponent;
private _dashboardTab!: DashboardTab;
private _migrationsTab!: MigrationsTab;
private _disposables: vscode.Disposable[] = [];
private isRefreshing: boolean = false;
public onDialogClosed = async (): Promise<void> => {
const label = await getSelectedServiceStatus();
this._serviceContextButton.label = label;
this._serviceContextButton.title = label;
await this.refreshMigrations();
};
constructor(context: vscode.ExtensionContext) {
this._context = context;
}
public errorTitle: string = '';
public errorLabel: string = '';
public errorDescription: string = '';
public async showError(errorTitle: string, errorLabel: string, errorDescription: string): Promise<void> {
this.errorTitle = errorTitle;
this.errorLabel = errorLabel;
this.errorDescription = errorDescription;
this._statusInfoBox.style = 'error';
this._statusInfoBox.text = errorTitle;
await this._updateStatusDisplay(this._statusInfoBox, true);
}
public async clearError(): Promise<void> {
await this._updateStatusDisplay(this._statusInfoBox, false);
this.errorTitle = '';
this.errorLabel = '';
this.errorDescription = '';
this._statusInfoBox.style = 'success';
this._statusInfoBox.text = '';
}
public register(): void {
azdata.ui.registerModelViewProvider('migration.dashboard', async (view) => {
azdata.ui.registerModelViewProvider('migration-dashboard', async (view) => {
this._view = view;
const container = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: '100%',
height: '100%'
}).component();
const header = this.createHeader(view);
// Files need to have the vscode-file scheme to be loaded by ADS
const watermarkUri = vscode.Uri
.file(<string>IconPathHelper.migrationDashboardHeaderBackground.light)
.with({ scheme: 'vscode-file' });
container.addItem(header, {
CSSStyles: {
'background-image': `
url(${watermarkUri}),
linear-gradient(0deg, rgba(0, 0, 0, 0.05) 0%, rgba(0, 0, 0, 0) 100%)
`,
'background-repeat': 'no-repeat',
'background-position': '91.06% 100%',
'margin-bottom': '20px'
}
});
const tasksContainer = await this.createTasks(view);
header.addItem(tasksContainer, {
CSSStyles: {
'width': `${maxWidth}px`,
'margin': '24px'
}
});
container.addItem(await this.createFooter(view), {
CSSStyles: {
'margin': '0 24px'
}
});
this._disposables.push(
this._view.onClosed(e => {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
}));
await view.initializeModel(container);
await this.refreshMigrations();
const openMigrationFcn = async (filter: AdsMigrationStatus): Promise<void> => {
this._tabs.selectTab(MigrationsTabId);
await this._migrationsTab.setMigrationFilter(filter);
};
this._dashboardTab = await new DashboardTab().create(
view,
async (filter: AdsMigrationStatus) => await openMigrationFcn(filter),
this);
this._disposables.push(this._dashboardTab);
this._migrationsTab = await new MigrationsTab().create(
this._context,
view,
this);
this._disposables.push(this._migrationsTab);
this._tabs = view.modelBuilder.tabbedPanel()
.withTabs([this._dashboardTab, this._migrationsTab])
.withLayout({ alwaysShowTabs: true, orientation: azdata.TabOrientation.Horizontal })
.withProps({
CSSStyles: {
'margin': '0px',
'padding': '0px',
'width': '100%'
}
})
.component();
this._disposables.push(
this._tabs.onTabChanged(
async id => {
await this.clearError();
await this.onDialogClosed();
}));
this._statusInfoBox = view.modelBuilder.infoBox()
.withProps({
style: 'error',
text: '',
announceText: true,
isClickable: true,
display: 'none',
CSSStyles: { 'font-size': '14px' },
}).component();
this._disposables.push(
this._statusInfoBox.onDidClick(
async e => await this.openErrorDialog()));
const flexContainer = view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([this._statusInfoBox, this._tabs])
.component();
await view.initializeModel(flexContainer);
await this.refresh();
});
}
private createHeader(view: azdata.ModelView): azdata.FlexContainer {
const header = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: maxWidth,
}).component();
const titleComponent = view.modelBuilder.text().withProps({
value: loc.DASHBOARD_TITLE,
width: '750px',
CSSStyles: {
...styles.DASHBOARD_TITLE_CSS
}
}).component();
const descriptionComponent = view.modelBuilder.text().withProps({
value: loc.DASHBOARD_DESCRIPTION,
CSSStyles: {
...styles.NOTE_CSS
}
}).component();
header.addItems([titleComponent, descriptionComponent], {
CSSStyles: {
'width': `${maxWidth}px`,
'padding-left': '24px'
}
});
return header;
public async refresh(): Promise<void> {
void this._migrationsTab.refresh();
await this._dashboardTab.refresh();
}
private async createTasks(view: azdata.ModelView): Promise<azdata.Component> {
const tasksContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'row',
width: '100%',
}).component();
const migrateButtonMetadata: IActionMetadata = {
title: loc.DASHBOARD_MIGRATE_TASK_BUTTON_TITLE,
description: loc.DASHBOARD_MIGRATE_TASK_BUTTON_DESCRIPTION,
iconPath: IconPathHelper.sqlMigrationLogo,
command: 'sqlmigration.start'
};
const preRequisiteListTitle = view.modelBuilder.text().withProps({
value: loc.PRE_REQ_TITLE,
CSSStyles: {
...styles.BODY_CSS,
'margin': '0px',
}
}).component();
const migrateButton = this.createTaskButton(view, migrateButtonMetadata);
const preRequisiteListElement = view.modelBuilder.text().withProps({
value: [
loc.PRE_REQ_1,
loc.PRE_REQ_2,
loc.PRE_REQ_3
],
CSSStyles: {
...styles.SMALL_NOTE_CSS,
'padding-left': '12px',
'margin': '-0.5em 0px',
}
}).component();
const preRequisiteLearnMoreLink = view.modelBuilder.hyperlink().withProps({
label: loc.LEARN_MORE,
ariaLabel: loc.LEARN_MORE_ABOUT_PRE_REQS,
url: 'https://aka.ms/azuresqlmigrationextension',
}).component();
const preReqContainer = view.modelBuilder.flexContainer().withItems([
preRequisiteListTitle,
preRequisiteListElement,
preRequisiteLearnMoreLink
]).withLayout({
flexFlow: 'column'
}).component();
tasksContainer.addItem(migrateButton, {});
tasksContainer.addItems([preReqContainer], {
CSSStyles: {
'margin-left': '20px'
}
});
return tasksContainer;
public async onDialogClosed(): Promise<void> {
await this._dashboardTab.onDialogClosed();
await this._migrationsTab.onDialogClosed();
}
private createTaskButton(view: azdata.ModelView, taskMetaData: IActionMetadata): azdata.Component {
const maxHeight: number = 84;
const maxWidth: number = 236;
const buttonContainer = view.modelBuilder.button().withProps({
buttonType: azdata.ButtonType.Informational,
description: taskMetaData.description,
height: maxHeight,
iconHeight: 32,
iconPath: taskMetaData.iconPath,
iconWidth: 32,
label: taskMetaData.title,
title: taskMetaData.title,
width: maxWidth,
CSSStyles: {
'border': '1px solid',
'display': 'flex',
'flex-direction': 'column',
'justify-content': 'flex-start',
'border-radius': '4px',
'transition': 'all .5s ease',
}
}).component();
this._disposables.push(
buttonContainer.onDidClick(async () => {
if (taskMetaData.command) {
await vscode.commands.executeCommand(taskMetaData.command);
}
}));
return view.modelBuilder.divContainer().withItems([buttonContainer]).component();
}
private _errorDialogIsOpen: boolean = false;
public async refreshMigrations(): Promise<void> {
if (this.isRefreshing) {
protected async openErrorDialog(): Promise<void> {
if (this._errorDialogIsOpen) {
return;
}
this.isRefreshing = true;
this._migrationStatusCardLoadingContainer.loading = true;
let migrations: DatabaseMigration[] = [];
try {
migrations = await getCurrentMigrations();
} catch (e) {
logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', e);
void vscode.window.showErrorMessage(loc.DASHBOARD_REFRESH_MIGRATIONS(e.message));
}
const tab = azdata.window.createTab(this.errorTitle);
tab.registerContent(async (view) => {
const flex = view.modelBuilder.flexContainer()
.withItems([
view.modelBuilder.text()
.withProps({ value: this.errorLabel, CSSStyles: { 'margin': '0px 0px 5px 5px' } })
.component(),
view.modelBuilder.inputBox()
.withProps({
value: this.errorDescription,
readOnly: true,
multiline: true,
inputType: 'text',
rows: 20,
CSSStyles: { 'overflow': 'hidden auto', 'margin': '0px 0px 0px 5px' },
})
.component()
])
.withLayout({
flexFlow: 'column',
width: 420,
})
.withProps({ CSSStyles: { 'margin': '0 10px 0 10px' } })
.component();
const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
let warningCount = 0;
for (let i = 0; i < inProgressMigrations.length; i++) {
if (inProgressMigrations[i].properties.migrationFailureError?.message ||
inProgressMigrations[i].properties.migrationStatusDetails?.fileUploadBlockingErrors ||
inProgressMigrations[i].properties.migrationStatusDetails?.restoreBlockingReason) {
warningCount += 1;
}
}
if (warningCount > 0) {
this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount);
this._inProgressMigrationButton.container.display = 'none';
this._inProgressWarningMigrationButton.container.display = '';
} else {
this._inProgressMigrationButton.container.display = '';
this._inProgressWarningMigrationButton.container.display = 'none';
}
await view.initializeModel(flex);
});
this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString();
this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString();
this._updateStatusCard(migrations, this._successfulMigrationButton, AdsMigrationStatus.SUCCEEDED, true);
this._updateStatusCard(migrations, this._failedMigrationButton, AdsMigrationStatus.FAILED);
this._updateStatusCard(migrations, this._completingMigrationButton, AdsMigrationStatus.COMPLETING);
this._updateStatusCard(migrations, this._allMigrationButton, AdsMigrationStatus.ALL, true);
await this._updateSummaryStatus();
this.isRefreshing = false;
this._migrationStatusCardLoadingContainer.loading = false;
}
private _updateStatusCard(
migrations: DatabaseMigration[],
card: StatusCard,
status: AdsMigrationStatus,
show?: boolean): void {
const list = filterMigrations(migrations, status);
const count = list?.length || 0;
card.container.display = count > 0 || show ? '' : 'none';
card.count.value = count.toString();
}
private createStatusCard(
cardIconPath: IconPath,
cardTitle: string,
hasSubtext: boolean = false
): StatusCard {
const buttonWidth = '400px';
const buttonHeight = hasSubtext ? '70px' : '50px';
const statusCard = this._view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
'width': buttonWidth,
'height': buttonHeight,
'align-items': 'center',
}
}).component();
const statusIcon = this._view.modelBuilder.image()
.withProps({
iconPath: cardIconPath!.light,
iconHeight: 24,
iconWidth: 24,
height: 32,
CSSStyles: { 'margin': '0 8px' }
}).component();
const textContainer = this._view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.component();
const cardTitleText = this._view.modelBuilder.text()
.withProps({ value: cardTitle })
.withProps({
CSSStyles: {
...styles.SECTION_HEADER_CSS,
'width': '240px',
}
}).component();
textContainer.addItem(cardTitleText);
const cardCount = this._view.modelBuilder.text().withProps({
value: '0',
CSSStyles: {
...styles.BIG_NUMBER_CSS,
'margin': '0 0 0 8px',
'text-align': 'center',
}
}).component();
let warningContainer;
let warningText;
if (hasSubtext) {
const warningIcon = this._view.modelBuilder.image()
.withProps({
iconPath: IconPathHelper.warning,
iconWidth: 12,
iconHeight: 12,
width: 12,
height: 18,
}).component();
const warningDescription = '';
warningText = this._view.modelBuilder.text().withProps({ value: warningDescription })
.withProps({
CSSStyles: {
...styles.BODY_CSS,
'padding-left': '8px',
const dialog = azdata.window.createModelViewDialog(
this.errorTitle,
'errorDialog',
450,
'flyout');
dialog.content = [tab];
dialog.okButton.label = loc.ERROR_DIALOG_CLEAR_BUTTON_LABEL;
dialog.okButton.focused = true;
dialog.cancelButton.label = loc.CLOSE;
this._disposables.push(
dialog.onClosed(async e => {
if (e === 'ok') {
await this.clearError();
}
}).component();
this._errorDialogIsOpen = false;
}));
warningContainer = this._view.modelBuilder.flexContainer()
.withItems(
[warningIcon, warningText],
{ flex: '0 0 auto' })
.withProps({
CSSStyles: { 'align-items': 'center' }
}).component();
textContainer.addItem(warningContainer);
azdata.window.openDialog(dialog);
} catch (error) {
this._errorDialogIsOpen = false;
}
statusCard.addItems([
statusIcon,
textContainer,
cardCount,
]);
const compositeButton = this._view.modelBuilder.divContainer()
.withItems([statusCard])
.withProps({
ariaRole: 'button',
ariaLabel: loc.SHOW_STATUS,
clickable: true,
CSSStyles: {
'height': buttonHeight,
'margin-bottom': '16px',
'border': '1px solid',
'display': 'flex',
'flex-direction': 'column',
'justify-content': 'flex-start',
'border-radius': '4px',
'transition': 'all .5s ease',
}
}).component();
return {
container: compositeButton,
count: cardCount,
textContainer: textContainer,
warningContainer: warningContainer,
warningText: warningText
};
}
private async createFooter(view: azdata.ModelView): Promise<azdata.Component> {
const footerContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'row',
width: maxWidth,
justifyContent: 'flex-start'
}).component();
const statusContainer = await this.createMigrationStatusContainer(view);
const videoLinksContainer = this.createVideoLinks(view);
footerContainer.addItem(statusContainer);
footerContainer.addItem(videoLinksContainer, {
CSSStyles: {
'padding-left': '8px',
}
});
return footerContainer;
}
private async createMigrationStatusContainer(view: azdata.ModelView): Promise<azdata.FlexContainer> {
const statusContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: '400px',
height: '385px',
justifyContent: 'flex-start',
}).withProps({
CSSStyles: {
'border': '1px solid rgba(0, 0, 0, 0.1)',
'padding': '10px',
}
}).component();
const statusContainerTitle = view.modelBuilder.text()
.withProps({
value: loc.DATABASE_MIGRATION_STATUS,
width: '100%',
CSSStyles: { ...styles.SECTION_HEADER_CSS }
}).component();
this._refreshButton = view.modelBuilder.button()
.withProps({
label: loc.REFRESH,
iconPath: IconPathHelper.refresh,
iconHeight: 16,
iconWidth: 16,
width: 70,
CSSStyles: { 'float': 'right' }
}).component();
const statusHeadingContainer = view.modelBuilder.flexContainer()
.withItems([
statusContainerTitle,
this._refreshButton,
]).withLayout({
alignContent: 'center',
alignItems: 'center',
flexFlow: 'row',
}).component();
this._disposables.push(
this._refreshButton.onDidClick(async (e) => {
this._refreshButton.enabled = false;
await this.refreshMigrations();
this._refreshButton.enabled = true;
}));
const buttonContainer = view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
'justify-content': 'left',
'align-iems': 'center',
},
})
.component();
buttonContainer.addItem(
await this.createServiceSelector(this._view));
this._selectServiceText = view.modelBuilder.text()
.withProps({
value: loc.SELECT_SERVICE_MESSAGE,
CSSStyles: {
'font-size': '12px',
'margin': '10px',
'font-weight': '350',
'text-align': 'center',
'display': 'none'
}
}).component();
const header = view.modelBuilder.flexContainer()
.withItems([statusHeadingContainer, buttonContainer])
.withLayout({ flexFlow: 'column', })
.component();
this._migrationStatusCardsContainer = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column',
height: '272px',
})
.withProps({ CSSStyles: { 'overflow': 'hidden auto' } })
.component();
await this._updateSummaryStatus();
// in progress
this._inProgressMigrationButton = this.createStatusCard(
IconPathHelper.inProgressMigration,
loc.MIGRATION_IN_PROGRESS);
this._disposables.push(
this._inProgressMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(
this._context,
AdsMigrationStatus.ONGOING,
this.onDialogClosed);
await dialog.initialize();
}));
this._migrationStatusCardsContainer.addItem(
this._inProgressMigrationButton.container,
{ flex: '0 0 auto' });
// in progress warning
this._inProgressWarningMigrationButton = this.createStatusCard(
IconPathHelper.inProgressMigration,
loc.MIGRATION_IN_PROGRESS,
true);
this._disposables.push(
this._inProgressWarningMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(
this._context,
AdsMigrationStatus.ONGOING,
this.onDialogClosed);
await dialog.initialize();
}));
this._migrationStatusCardsContainer.addItem(
this._inProgressWarningMigrationButton.container,
{ flex: '0 0 auto' });
// successful
this._successfulMigrationButton = this.createStatusCard(
IconPathHelper.completedMigration,
loc.MIGRATION_COMPLETED);
this._disposables.push(
this._successfulMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(
this._context,
AdsMigrationStatus.SUCCEEDED,
this.onDialogClosed);
await dialog.initialize();
}));
this._migrationStatusCardsContainer.addItem(
this._successfulMigrationButton.container,
{ flex: '0 0 auto' });
// completing
this._completingMigrationButton = this.createStatusCard(
IconPathHelper.completingCutover,
loc.MIGRATION_CUTOVER_CARD);
this._disposables.push(
this._completingMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(
this._context,
AdsMigrationStatus.COMPLETING,
this.onDialogClosed);
await dialog.initialize();
}));
this._migrationStatusCardsContainer.addItem(
this._completingMigrationButton.container,
{ flex: '0 0 auto' });
// failed
this._failedMigrationButton = this.createStatusCard(
IconPathHelper.error,
loc.MIGRATION_FAILED);
this._disposables.push(
this._failedMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(
this._context,
AdsMigrationStatus.FAILED,
this.onDialogClosed);
await dialog.initialize();
}));
this._migrationStatusCardsContainer.addItem(
this._failedMigrationButton.container,
{ flex: '0 0 auto' });
// all migrations
this._allMigrationButton = this.createStatusCard(
IconPathHelper.view,
loc.VIEW_ALL);
this._disposables.push(
this._allMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(
this._context,
AdsMigrationStatus.ALL,
this.onDialogClosed);
await dialog.initialize();
}));
this._migrationStatusCardsContainer.addItem(
this._allMigrationButton.container,
{ flex: '0 0 auto' });
this._migrationStatusCardLoadingContainer = view.modelBuilder.loadingComponent()
.withItem(this._migrationStatusCardsContainer)
.component();
statusContainer.addItem(header, { CSSStyles: { 'margin-bottom': '10px' } });
statusContainer.addItem(this._selectServiceText, {});
statusContainer.addItem(this._migrationStatusCardLoadingContainer, {});
return statusContainer;
}
private async _updateSummaryStatus(): Promise<void> {
const serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
const isContextValid = isServiceContextValid(serviceContext);
await this._selectServiceText.updateCssStyles({ 'display': isContextValid ? 'none' : 'block' });
await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': isContextValid ? 'visible' : 'hidden' });
this._refreshButton.enabled = isContextValid;
}
private async createServiceSelector(view: azdata.ModelView): Promise<azdata.Component> {
const serviceContextLabel = await getSelectedServiceStatus();
this._serviceContextButton = view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.sqlMigrationService,
iconHeight: 22,
iconWidth: 22,
label: serviceContextLabel,
title: serviceContextLabel,
description: loc.MIGRATION_SERVICE_DESCRIPTION,
buttonType: azdata.ButtonType.Informational,
width: 375,
CSSStyles: { ...BUTTON_CSS },
})
.component();
this._disposables.push(
this._serviceContextButton.onDidClick(async () => {
const dialog = new SelectMigrationServiceDialog(this.onDialogClosed);
await dialog.initialize();
}));
return this._serviceContextButton;
}
private createVideoLinks(view: azdata.ModelView): azdata.Component {
const linksContainer = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column',
width: '440px',
height: '385px',
justifyContent: 'flex-start',
}).withProps({
CSSStyles: {
'border': '1px solid rgba(0, 0, 0, 0.1)',
'padding': '10px',
'overflow': 'scroll',
}
}).component();
const titleComponent = view.modelBuilder.text().withProps({
value: loc.HELP_TITLE,
CSSStyles: {
...styles.SECTION_HEADER_CSS
}
}).component();
linksContainer.addItems([titleComponent], {
CSSStyles: {
'margin-bottom': '16px'
}
});
const links = [
{
title: localize('sql.migration.dashboard.help.link.migrateUsingADS', 'Migrate databases using Azure Data Studio'),
description: localize('sql.migration.dashboard.help.description.migrateUsingADS', 'The Azure SQL Migration extension for Azure Data Studio provides capabilities to assess, get right-sized Azure recommendations and migrate SQL Server databases to Azure.'),
link: 'https://docs.microsoft.com/azure/dms/migration-using-azure-data-studio'
},
{
title: localize('sql.migration.dashboard.help.link.mi', 'Tutorial: Migrate to Azure SQL Managed Instance (online)'),
description: localize('sql.migration.dashboard.help.description.mi', 'A step-by-step tutorial to migrate databases from a SQL Server instance (on-premises or Azure Virtual Machines) to Azure SQL Managed Instance with minimal downtime.'),
link: 'https://docs.microsoft.com/azure/dms/tutorial-sql-server-managed-instance-online-ads'
},
{
title: localize('sql.migration.dashboard.help.link.vm', 'Tutorial: Migrate to SQL Server on Azure Virtual Machines (online)'),
description: localize('sql.migration.dashboard.help.description.vm', 'A step-by-step tutorial to migrate databases from a SQL Server instance (on-premises) to SQL Server on Azure Virtual Machines with minimal downtime.'),
link: 'https://docs.microsoft.com/azure/dms/tutorial-sql-server-to-virtual-machine-online-ads'
},
{
title: localize('sql.migration.dashboard.help.link.dmsGuide', 'Azure Database Migration Guides'),
description: localize('sql.migration.dashboard.help.description.dmsGuide', 'A hub of migration articles that provides step-by-step guidance for migrating and modernizing your data assets in Azure.'),
link: 'https://docs.microsoft.com/data-migration/'
},
];
linksContainer.addItems(links.map(l => this.createLink(view, l)), {});
const videoLinks: IActionMetadata[] = [];
const videosContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'row',
width: maxWidth,
}).component();
videosContainer.addItems(videoLinks.map(l => this.createVideoLink(view, l)), {});
linksContainer.addItem(videosContainer);
return linksContainer;
}
private createLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
const maxWidth = 400;
const labelsContainer = view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'flex-direction': 'column',
'width': `${maxWidth}px`,
'justify-content': 'flex-start',
'margin-bottom': '12px'
}
}).component();
const linkContainer = view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'flex-direction': 'row',
'width': `${maxWidth}px`,
'justify-content': 'flex-start',
'margin-bottom': '4px'
}
}).component();
const descriptionComponent = view.modelBuilder.text().withProps({
value: linkMetaData.description,
width: maxWidth,
CSSStyles: {
...styles.NOTE_CSS
}
}).component();
const linkComponent = view.modelBuilder.hyperlink().withProps({
label: linkMetaData.title!,
url: linkMetaData.link!,
showLinkIcon: true,
CSSStyles: {
...styles.BODY_CSS
}
}).component();
linkContainer.addItem(linkComponent);
labelsContainer.addItems([linkContainer, descriptionComponent]);
return labelsContainer;
}
private createVideoLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
const maxWidth = 150;
const videosContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column',
width: maxWidth,
justifyContent: 'flex-start'
}).component();
const video1Container = view.modelBuilder.divContainer().withProps({
clickable: true,
width: maxWidth,
height: '100px'
}).component();
const descriptionComponent = view.modelBuilder.text().withProps({
value: linkMetaData.description,
width: maxWidth,
height: '50px',
CSSStyles: {
...styles.BODY_CSS
}
}).component();
this._disposables.push(
video1Container.onDidClick(async () => {
if (linkMetaData.link) {
await vscode.env.openExternal(vscode.Uri.parse(linkMetaData.link));
}
}));
videosContainer.addItem(video1Container, {
CSSStyles: {
'background-image': `url(${vscode.Uri.file(<string>linkMetaData.iconPath?.light)})`,
'background-repeat': 'no-repeat',
'background-position': 'top',
'width': `${maxWidth}px`,
'height': '104px',
'background-size': `${maxWidth}px 120px`
}
});
videosContainer.addItem(descriptionComponent);
return videosContainer;
private async _updateStatusDisplay(control: azdata.Component, visible: boolean): Promise<void> {
await control.updateCssStyles({ 'display': visible ? 'inline' : 'none' });
}
}

View File

@@ -0,0 +1,227 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as loc from '../constants/strings';
import { IconPathHelper } from '../constants/iconPathHelper';
import { EOL } from 'os';
import { DatabaseMigration } from '../api/azure';
import { DashboardStatusBar } from './sqlServerDashboard';
import { getSelectedServiceStatus } from '../models/migrationLocalStorage';
export const SqlMigrationExtensionId = 'microsoft.sql-migration';
export const EmptySettingValue = '-';
export enum AdsMigrationStatus {
ALL = 'all',
ONGOING = 'ongoing',
SUCCEEDED = 'succeeded',
FAILED = 'failed',
COMPLETING = 'completing'
}
export const MenuCommands = {
Cutover: 'sqlmigration.cutover',
ViewDatabase: 'sqlmigration.view.database',
ViewTarget: 'sqlmigration.view.target',
ViewService: 'sqlmigration.view.service',
CopyMigration: 'sqlmigration.copy.migration',
CancelMigration: 'sqlmigration.cancel.migration',
RetryMigration: 'sqlmigration.retry.migration',
StartMigration: 'sqlmigration.start',
IssueReporter: 'workbench.action.openIssueReporter',
};
export abstract class TabBase<T> implements azdata.Tab, vscode.Disposable {
public content!: azdata.Component;
public title: string = '';
public id!: string;
public icon!: azdata.IconPath | undefined;
protected context!: vscode.ExtensionContext;
protected view!: azdata.ModelView;
protected disposables: vscode.Disposable[] = [];
protected isRefreshing: boolean = false;
protected openMigrationFcn!: (status: AdsMigrationStatus) => Promise<void>;
protected statusBar!: DashboardStatusBar;
protected abstract initialize(view: azdata.ModelView): Promise<void>;
public abstract refresh(): Promise<void>;
dispose() {
this.disposables.forEach(
d => { try { d.dispose(); } catch { } });
}
protected numberCompare(number1: number | undefined, number2: number | undefined, sortDir: number): number {
if (!number1) {
return sortDir;
} else if (!number2) {
return -sortDir;
}
return number1 > number2 ? -sortDir : sortDir;
}
protected stringCompare(string1: string | undefined, string2: string | undefined, sortDir: number): number {
if (!string1) {
return sortDir;
} else if (!string2) {
return -sortDir;
}
return string1.localeCompare(string2) * -sortDir;
}
protected dateCompare(stringDate1: string | undefined, stringDate2: string | undefined, sortDir: number): number {
if (!stringDate1) {
return sortDir;
} else if (!stringDate2) {
return -sortDir;
}
return new Date(stringDate1) > new Date(stringDate2) ? -sortDir : sortDir;
}
protected async updateServiceContext(button: azdata.ButtonComponent): Promise<void> {
const label = await getSelectedServiceStatus();
if (button.label !== label ||
button.title !== label) {
button.label = label;
button.title = label;
await this.refresh();
}
}
protected createNewMigrationButton(): azdata.ButtonComponent {
const newMigrationButton = this.view.modelBuilder.button()
.withProps({
buttonType: azdata.ButtonType.Normal,
label: loc.DESKTOP_MIGRATION_BUTTON_LABEL,
description: loc.DESKTOP_MIGRATION_BUTTON_DESCRIPTION,
height: 24,
iconHeight: 24,
iconWidth: 24,
iconPath: IconPathHelper.addNew,
}).component();
this.disposables.push(
newMigrationButton.onDidClick(async () => {
const actionId = MenuCommands.StartMigration;
const args = {
extensionId: SqlMigrationExtensionId,
issueTitle: loc.DASHBOARD_MIGRATE_TASK_BUTTON_TITLE,
};
return await vscode.commands.executeCommand(actionId, args);
}));
return newMigrationButton;
}
protected createNewSupportRequestButton(): azdata.ButtonComponent {
const newSupportRequestButton = this.view.modelBuilder.button()
.withProps({
buttonType: azdata.ButtonType.Normal,
label: loc.DESKTOP_SUPPORT_BUTTON_LABEL,
description: loc.DESKTOP_SUPPORT_BUTTON_DESCRIPTION,
height: 24,
iconHeight: 24,
iconWidth: 24,
iconPath: IconPathHelper.newSupportRequest,
}).component();
this.disposables.push(
newSupportRequestButton.onDidClick(async () => {
await vscode.env.openExternal(vscode.Uri.parse(
`https://portal.azure.com/#blade/Microsoft_Azure_Support/HelpAndSupportBlade/newsupportrequest`));
}));
return newSupportRequestButton;
}
protected createFeedbackButton(): azdata.ButtonComponent {
const feedbackButton = this.view.modelBuilder.button()
.withProps({
buttonType: azdata.ButtonType.Normal,
label: loc.DESKTOP_FEEDBACK_BUTTON_LABEL,
description: loc.DESKTOP_FEEDBACK_BUTTON_DESCRIPTION,
height: 24,
iconHeight: 24,
iconWidth: 24,
iconPath: IconPathHelper.sendFeedback,
}).component();
this.disposables.push(
feedbackButton.onDidClick(async () => {
const actionId = MenuCommands.IssueReporter;
const args = {
extensionId: SqlMigrationExtensionId,
issueTitle: loc.FEEDBACK_ISSUE_TITLE,
};
return await vscode.commands.executeCommand(actionId, args);
}));
return feedbackButton;
}
protected getMigrationErrors(migration: DatabaseMigration): string {
const errors = [];
errors.push(migration.properties.provisioningError);
errors.push(migration.properties.migrationFailureError?.message);
errors.push(...migration.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []);
errors.push(migration.properties.migrationStatusDetails?.restoreBlockingReason);
// remove undefined and duplicate error entries
return errors
.filter((e, i, arr) => e !== undefined && i === arr.indexOf(e))
.join(EOL);
}
protected showDialogMessage(
title: string,
statusMessage: string,
errorMessage: string,
): void {
const tab = azdata.window.createTab(title);
tab.registerContent(async (view) => {
const flex = view.modelBuilder.flexContainer()
.withItems([
view.modelBuilder.text()
.withProps({ value: statusMessage })
.component(),
])
.withLayout({
flexFlow: 'column',
width: 420,
})
.withProps({ CSSStyles: { 'margin': '0 15px' } })
.component();
if (errorMessage.length > 0) {
flex.addItem(
view.modelBuilder.inputBox()
.withProps({
value: errorMessage,
readOnly: true,
multiline: true,
inputType: 'text',
height: 100,
CSSStyles: { 'overflow': 'hidden auto' },
})
.component()
);
}
await view.initializeModel(flex);
});
const dialog = azdata.window.createModelViewDialog(
title,
'messageDialog',
450,
'normal');
dialog.content = [tab];
dialog.okButton.hidden = true;
dialog.cancelButton.focused = true;
dialog.cancelButton.label = loc.CLOSE;
azdata.window.openDialog(dialog);
}
}

View File

@@ -864,6 +864,7 @@ export class SqlDatabaseTree {
this._descriptionText.value = selectedIssue?.description || '';
this._moreInfo.url = selectedIssue?.helpLink || '';
this._moreInfo.label = selectedIssue?.displayName || '';
this._moreInfo.ariaLabel = selectedIssue?.displayName || '';
this._impactedObjects = selectedIssue?.impactedObjects || [];
this._recommendationText.value = selectedIssue?.message || constants.NA;

View File

@@ -278,6 +278,7 @@ export class CreateSqlMigrationServiceDialog {
this._createResourceGroupLink = this._view.modelBuilder.hyperlink().withProps({
label: constants.CREATE_NEW,
ariaLabel: constants.CREATE_NEW_RESOURCE_GROUP,
url: '',
CSSStyles: {
...styles.BODY_CSS

View File

@@ -11,7 +11,8 @@ import { getMigrationTargetInstance, SqlManagedInstance } from '../../api/azure'
import { IconPathHelper } from '../../constants/iconPathHelper';
import { convertByteSizeToReadableUnit, get12HourTime } from '../../api/utils';
import * as styles from '../../constants/styles';
import { isBlobMigration } from '../../constants/helper';
import { getMigrationTargetTypeEnum, isBlobMigration } from '../../constants/helper';
import { MigrationTargetType, ServiceTier } from '../../models/stateMachine';
export class ConfirmCutoverDialog {
private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView;
@@ -32,7 +33,7 @@ export class ConfirmCutoverDialog {
}).component();
const sourceDatabaseText = view.modelBuilder.text().withProps({
value: this.migrationCutoverModel._migration.properties.sourceDatabaseName,
value: this.migrationCutoverModel.migration.properties.sourceDatabaseName,
CSSStyles: {
...styles.SMALL_NOTE_CSS,
'margin': '4px 0px 8px'
@@ -53,7 +54,7 @@ export class ConfirmCutoverDialog {
}
}).component();
const fileContainer = isBlobMigration(this.migrationCutoverModel.migrationStatus)
const fileContainer = isBlobMigration(this.migrationCutoverModel.migration)
? this.createBlobFileContainer()
: this.createNetworkShareFileContainer();
@@ -76,13 +77,13 @@ export class ConfirmCutoverDialog {
}).component();
let infoDisplay = 'none';
if (this.migrationCutoverModel._migration.id.toLocaleLowerCase().includes('managedinstances')) {
if (getMigrationTargetTypeEnum(this.migrationCutoverModel.migration) === MigrationTargetType.SQLMI) {
const targetInstance = await getMigrationTargetInstance(
this.migrationCutoverModel._serviceConstext.azureAccount!,
this.migrationCutoverModel._serviceConstext.subscription!,
this.migrationCutoverModel._migration);
this.migrationCutoverModel.serviceConstext.azureAccount!,
this.migrationCutoverModel.serviceConstext.subscription!,
this.migrationCutoverModel.migration);
if ((<SqlManagedInstance>targetInstance)?.sku?.tier === 'BusinessCritical') {
if ((<SqlManagedInstance>targetInstance)?.sku?.tier === ServiceTier.BusinessCritical) {
infoDisplay = 'inline';
}
}
@@ -116,7 +117,7 @@ export class ConfirmCutoverDialog {
await this.migrationCutoverModel.startCutover();
void vscode.window.showInformationMessage(
constants.CUTOVER_IN_PROGRESS(
this.migrationCutoverModel._migration.properties.sourceDatabaseName));
this.migrationCutoverModel.migration.properties.sourceDatabaseName));
}));
const formBuilder = view.modelBuilder.formContainer().withFormItems(
@@ -163,7 +164,7 @@ export class ConfirmCutoverDialog {
} catch (e) {
this._dialogObject.message = {
level: azdata.window.MessageLevel.Error,
text: e.toString()
text: e.message
};
} finally {
refreshLoader.loading = false;
@@ -241,7 +242,7 @@ export class ConfirmCutoverDialog {
} catch (e) {
this._dialogObject.message = {
level: azdata.window.MessageLevel.Error,
text: e.toString()
text: e.message
};
} finally {
refreshLoader.loading = false;

View File

@@ -1,852 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { IconPathHelper } from '../../constants/iconPathHelper';
import { BackupFileInfoStatus, MigrationServiceContext, MigrationStatus } from '../../models/migrationLocalStorage';
import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
import * as loc from '../../constants/strings';
import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage, clearDialogMessage, displayDialogErrorMessage } from '../../api/utils';
import { EOL } from 'os';
import { ConfirmCutoverDialog } from './confirmCutoverDialog';
import { logError, TelemetryViews } from '../../telemtery';
import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog';
import * as styles from '../../constants/styles';
import { canRetryMigration, getMigrationStatus, isBlobMigration, isOfflineMigation } from '../../constants/helper';
import { DatabaseMigration, getResourceName } from '../../api/azure';
const statusImageSize: number = 14;
export class MigrationCutoverDialog {
private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView;
private _model: MigrationCutoverDialogModel;
private _databaseTitleName!: azdata.TextComponent;
private _cutoverButton!: azdata.ButtonComponent;
private _refreshButton!: azdata.ButtonComponent;
private _cancelButton!: azdata.ButtonComponent;
private _refreshLoader!: azdata.LoadingComponent;
private _copyDatabaseMigrationDetails!: azdata.ButtonComponent;
private _newSupportRequest!: azdata.ButtonComponent;
private _retryButton!: azdata.ButtonComponent;
private _sourceDatabaseInfoField!: InfoFieldSchema;
private _sourceDetailsInfoField!: InfoFieldSchema;
private _sourceVersionInfoField!: InfoFieldSchema;
private _targetDatabaseInfoField!: InfoFieldSchema;
private _targetServerInfoField!: InfoFieldSchema;
private _targetVersionInfoField!: InfoFieldSchema;
private _migrationStatusInfoField!: InfoFieldSchema;
private _fullBackupFileOnInfoField!: InfoFieldSchema;
private _backupLocationInfoField!: InfoFieldSchema;
private _lastLSNInfoField!: InfoFieldSchema;
private _lastAppliedBackupInfoField!: InfoFieldSchema;
private _lastAppliedBackupTakenOnInfoField!: InfoFieldSchema;
private _currentRestoringFileInfoField!: InfoFieldSchema;
private _fileCount!: azdata.TextComponent;
private _fileTable!: azdata.DeclarativeTableComponent;
private _disposables: vscode.Disposable[] = [];
private _emptyTableFill!: azdata.FlexContainer;
private isRefreshing = false;
readonly _infoFieldWidth: string = '250px';
constructor(
private readonly _context: vscode.ExtensionContext,
private readonly _serviceContext: MigrationServiceContext,
private readonly _migration: DatabaseMigration,
private readonly _onClosedCallback: () => Promise<void>) {
this._model = new MigrationCutoverDialogModel(_serviceContext, _migration);
this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 'wide');
}
async initialize(): Promise<void> {
let tab = azdata.window.createTab('');
tab.registerContent(async (view: azdata.ModelView) => {
try {
this._view = view;
this._fileCount = view.modelBuilder.text().withProps({
width: '500px',
CSSStyles: {
...styles.BODY_CSS
}
}).component();
const rowCssStyle: azdata.CssStyles = {
'border': 'none',
'text-align': 'left',
'border-bottom': '1px solid',
'font-size': '12px'
};
const headerCssStyles: azdata.CssStyles = {
'border': 'none',
'text-align': 'left',
'border-bottom': '1px solid',
'font-weight': 'bold',
'padding-left': '0px',
'padding-right': '0px',
'font-size': '12px'
};
this._fileTable = view.modelBuilder.declarativeTable().withProps({
ariaLabel: loc.ACTIVE_BACKUP_FILES,
columns: [
{
displayName: loc.ACTIVE_BACKUP_FILES,
valueType: azdata.DeclarativeDataType.string,
width: '230px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.TYPE,
valueType: azdata.DeclarativeDataType.string,
width: '90px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.STATUS,
valueType: azdata.DeclarativeDataType.string,
width: '60px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.DATA_UPLOADED,
valueType: azdata.DeclarativeDataType.string,
width: '120px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.COPY_THROUGHPUT,
valueType: azdata.DeclarativeDataType.string,
width: '150px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.BACKUP_START_TIME,
valueType: azdata.DeclarativeDataType.string,
width: '130px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.FIRST_LSN,
valueType: azdata.DeclarativeDataType.string,
width: '120px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.LAST_LSN,
valueType: azdata.DeclarativeDataType.string,
width: '120px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
}
],
data: [],
width: '1100px',
height: '300px',
CSSStyles: {
...styles.BODY_CSS,
'display': 'none',
'padding-left': '0px'
}
}).component();
const _emptyTableImage = view.modelBuilder.image().withProps({
iconPath: IconPathHelper.emptyTable,
iconHeight: '100px',
iconWidth: '100px',
height: '100px',
width: '100px',
CSSStyles: {
'text-align': 'center'
}
}).component();
const _emptyTableText = view.modelBuilder.text().withProps({
value: loc.EMPTY_TABLE_TEXT,
CSSStyles: {
...styles.NOTE_CSS,
'margin-top': '8px',
'text-align': 'center',
'width': '300px'
}
}).component();
this._emptyTableFill = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column',
alignItems: 'center'
}).withItems([
_emptyTableImage,
_emptyTableText,
]).withProps({
width: 1000,
display: 'none'
}).component();
let formItems = [
{ component: this.migrationContainerHeader() },
{ component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component() },
{ component: await this.migrationInfoGrid() },
{ component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component() },
{ component: this._fileCount },
{ component: this._fileTable },
{ component: this._emptyTableFill }
];
const formBuilder = view.modelBuilder.formContainer().withFormItems(
formItems,
{ horizontal: false }
);
const form = formBuilder.withLayout({ width: '100%' }).component();
this._disposables.push(
this._view.onClosed(e => {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
}));
await view.initializeModel(form);
await this.refreshStatus();
} catch (e) {
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
}
});
this._dialogObject.content = [tab];
this._dialogObject.cancelButton.hidden = true;
this._dialogObject.okButton.label = loc.CLOSE;
azdata.window.openDialog(this._dialogObject);
}
private migrationContainerHeader(): azdata.FlexContainer {
const sqlDatbaseLogo = this._view.modelBuilder.image().withProps({
iconPath: IconPathHelper.sqlDatabaseLogo,
iconHeight: '32px',
iconWidth: '32px',
width: '32px',
height: '32px'
}).component();
this._databaseTitleName = this._view.modelBuilder.text().withProps({
CSSStyles: {
...styles.PAGE_TITLE_CSS
},
width: 950,
value: this._model._migration.properties.sourceDatabaseName
}).component();
const databaseSubTitle = this._view.modelBuilder.text().withProps({
CSSStyles: {
...styles.NOTE_CSS
},
width: 950,
value: loc.DATABASE
}).component();
const titleContainer = this._view.modelBuilder.flexContainer().withItems([
this._databaseTitleName,
databaseSubTitle
]).withLayout({
'flexFlow': 'column'
}).withProps({
width: 950
}).component();
const titleLogoContainer = this._view.modelBuilder.flexContainer().withProps({
width: 1000
}).component();
titleLogoContainer.addItem(sqlDatbaseLogo, {
flex: '0'
});
titleLogoContainer.addItem(titleContainer, {
flex: '0',
CSSStyles: {
'margin-left': '5px',
'width': '930px'
}
});
const headerActions = this._view.modelBuilder.flexContainer().withLayout({
}).withProps({
width: 1000
}).component();
this._cutoverButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.cutover,
iconHeight: '16px',
iconWidth: '16px',
label: loc.COMPLETE_CUTOVER,
height: '20px',
width: '140px',
enabled: false,
CSSStyles: {
...styles.BODY_CSS,
'display': isOfflineMigation(this._model._migration) ? 'none' : 'block'
}
}).component();
this._disposables.push(this._cutoverButton.onDidClick(async (e) => {
await this.refreshStatus();
const dialog = new ConfirmCutoverDialog(this._model);
await dialog.initialize();
if (this._model.CutoverError) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CUTOVER_ERROR, this._model.CutoverError);
}
}));
headerActions.addItem(this._cutoverButton, { flex: '0' });
this._cancelButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.cancel,
iconHeight: '16px',
iconWidth: '16px',
label: loc.CANCEL_MIGRATION,
height: '20px',
width: '140px',
enabled: false,
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
this._disposables.push(this._cancelButton.onDidClick((e) => {
void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, { modal: true }, loc.YES, loc.NO).then(async (v) => {
if (v === loc.YES) {
await this._model.cancelMigration();
await this.refreshStatus();
if (this._model.CancelMigrationError) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CANCELLATION_ERROR, this._model.CancelMigrationError);
}
}
});
}));
headerActions.addItem(this._cancelButton, {
flex: '0'
});
this._retryButton = this._view.modelBuilder.button().withProps({
label: loc.RETRY_MIGRATION,
iconPath: IconPathHelper.retry,
enabled: false,
iconHeight: '16px',
iconWidth: '16px',
height: '20px',
width: '120px',
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
this._disposables.push(this._retryButton.onDidClick(
async (e) => {
await this.refreshStatus();
const retryMigrationDialog = new RetryMigrationDialog(
this._context,
this._serviceContext,
this._migration,
this._onClosedCallback);
await retryMigrationDialog.openDialog();
}
));
headerActions.addItem(this._retryButton, {
flex: '0',
});
this._refreshButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.refresh,
iconHeight: '16px',
iconWidth: '16px',
label: 'Refresh',
height: '20px',
width: '80px',
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
this._disposables.push(
this._refreshButton.onDidClick(async (e) => {
this._refreshButton.enabled = false;
await this.refreshStatus();
this._refreshButton.enabled = true;
}));
headerActions.addItem(this._refreshButton, { flex: '0' });
this._copyDatabaseMigrationDetails = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.copy,
iconHeight: '16px',
iconWidth: '16px',
label: loc.COPY_MIGRATION_DETAILS,
height: '20px',
width: '160px',
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
this._disposables.push(this._copyDatabaseMigrationDetails.onDidClick(async (e) => {
await this.refreshStatus();
await vscode.env.clipboard.writeText(this.getMigrationDetails());
void vscode.window.showInformationMessage(loc.DETAILS_COPIED);
}));
headerActions.addItem(this._copyDatabaseMigrationDetails, {
flex: '0',
CSSStyles: { 'margin-left': '5px' }
});
// create new support request button. Hiding button until sql migration support has been setup.
this._newSupportRequest = this._view.modelBuilder.button().withProps({
label: loc.NEW_SUPPORT_REQUEST,
iconPath: IconPathHelper.newSupportRequest,
iconHeight: '16px',
iconWidth: '16px',
height: '20px',
width: '160px',
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
this._disposables.push(this._newSupportRequest.onDidClick(async (e) => {
const serviceId = this._model._migration.properties.migrationService;
const supportUrl = `https://portal.azure.com/#resource${serviceId}/supportrequest`;
await vscode.env.openExternal(vscode.Uri.parse(supportUrl));
}));
headerActions.addItem(this._newSupportRequest, {
flex: '0',
CSSStyles: {
'margin-left': '5px'
}
});
this._refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
loading: false,
CSSStyles: {
'height': '8px',
'margin-top': '4px'
}
}).component();
headerActions.addItem(this._refreshLoader, {
flex: '0',
CSSStyles: {
'margin-left': '16px'
}
});
const header = this._view.modelBuilder.flexContainer().withItems([
titleLogoContainer
]).withLayout({
flexFlow: 'column'
}).withProps({
CSSStyles: {
width: 1000
}
}).component();
header.addItem(headerActions, {
'CSSStyles': {
'margin-top': '16px'
}
});
return header;
}
private async migrationInfoGrid(): Promise<azdata.FlexContainer> {
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
container.addItem(infoField.flexContainer, {
CSSStyles: {
width: this._infoFieldWidth,
}
});
};
const flexServer = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).component();
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
const flexTarget = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).component();
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
const _isBlobMigration = isBlobMigration(this._model._migration);
const flexStatus = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).component();
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', _isBlobMigration);
this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
addInfoFieldToContainer(this._fullBackupFileOnInfoField, flexStatus);
addInfoFieldToContainer(this._backupLocationInfoField, flexStatus);
const flexFile = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).component();
this._lastLSNInfoField = await this.createInfoField(loc.LAST_APPLIED_LSN, '', _isBlobMigration);
this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', _isBlobMigration);
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', !_isBlobMigration);
addInfoFieldToContainer(this._lastLSNInfoField, flexFile);
addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
addInfoFieldToContainer(this._lastAppliedBackupTakenOnInfoField, flexFile);
addInfoFieldToContainer(this._currentRestoringFileInfoField, flexFile);
const flexInfoProps = {
flex: '0',
CSSStyles: {
'flex': '0',
'width': this._infoFieldWidth
}
};
const flexInfo = this._view.modelBuilder.flexContainer().withProps({
width: 1000
}).component();
flexInfo.addItem(flexServer, flexInfoProps);
flexInfo.addItem(flexTarget, flexInfoProps);
flexInfo.addItem(flexStatus, flexInfoProps);
flexInfo.addItem(flexFile, flexInfoProps);
return flexInfo;
}
private getMigrationDetails(): string {
return JSON.stringify(this._model.migrationStatus, undefined, 2);
}
private async refreshStatus(): Promise<void> {
if (this.isRefreshing) {
return;
}
try {
clearDialogMessage(this._dialogObject);
await this._cutoverButton.updateCssStyles(
{ 'display': isOfflineMigation(this._model._migration) ? 'none' : 'block' });
this.isRefreshing = true;
this._refreshLoader.loading = true;
await this._model.fetchStatus();
const errors = [];
errors.push(this._model.migrationStatus.properties.provisioningError);
errors.push(this._model.migrationStatus.properties.migrationFailureError?.message);
errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []);
errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.restoreBlockingReason);
this._dialogObject.message = {
// remove undefined and duplicate error entries
text: errors
.filter((e, i, arr) => e !== undefined && i === arr.indexOf(e))
.join(EOL),
level: this._model.migrationStatus.properties.migrationStatus === MigrationStatus.InProgress
|| this._model.migrationStatus.properties.migrationStatus === MigrationStatus.Completing
? azdata.window.MessageLevel.Warning
: azdata.window.MessageLevel.Error,
description: this.getMigrationDetails()
};
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
const sqlServerName = this._model._migration.properties.sourceServerName;
const sourceDatabaseName = this._model._migration.properties.sourceDatabaseName;
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
const targetDatabaseName = this._model._migration.name;
const targetServerName = getResourceName(this._model._migration.properties.scope);
let targetServerVersion;
if (this._model.migrationStatus.id.includes('managedInstances')) {
targetServerVersion = loc.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
} else {
targetServerVersion = loc.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
}
let lastAppliedSSN: string;
let lastAppliedBackupFileTakenOn: string;
const tableData: ActiveBackupFileSchema[] = [];
this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach(
(activeBackupSet) => {
if (this._shouldDisplayBackupFileTable()) {
tableData.push(
...activeBackupSet.listOfBackupFiles.map(f => {
return {
fileName: f.fileName,
type: activeBackupSet.backupType,
status: f.status,
dataUploaded: `${convertByteSizeToReadableUnit(f.dataWritten)} / ${convertByteSizeToReadableUnit(f.totalSize)}`,
copyThroughput: (f.copyThroughput) ? (f.copyThroughput / 1024).toFixed(2) : '-',
backupStartTime: activeBackupSet.backupStartDate,
firstLSN: activeBackupSet.firstLSN,
lastLSN: activeBackupSet.lastLSN
};
})
);
}
if (activeBackupSet.listOfBackupFiles[0].fileName === this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
lastAppliedSSN = activeBackupSet.lastLSN;
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
}
});
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
this._sourceDetailsInfoField.text.value = sqlServerName;
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
this._targetDatabaseInfoField.text.value = targetDatabaseName;
this._targetServerInfoField.text.value = targetServerName;
this._targetVersionInfoField.text.value = targetServerVersion;
const migrationStatusTextValue = this._getMigrationStatus();
this._migrationStatusInfoField.text.value = migrationStatusTextValue ?? '-';
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migrationStatusTextValue);
this._fullBackupFileOnInfoField.text.value = this._model.migrationStatus?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? '-';
let backupLocation;
const _isBlobMigration = isBlobMigration(this._model._migration);
// Displaying storage accounts and blob container for azure blob backups.
if (_isBlobMigration) {
const storageAccountResourceId = this._model._migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId;
const blobContainerName = this._model._migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName;
backupLocation = storageAccountResourceId && blobContainerName
? `${storageAccountResourceId?.split('/').pop()} - ${blobContainerName}`
: undefined;
} else {
const fileShare = this._model._migration.properties.backupConfiguration?.sourceLocation?.fileShare;
backupLocation = fileShare?.path! ?? '-';
}
this._backupLocationInfoField.text.value = backupLocation ?? '-';
this._lastLSNInfoField.text.value = lastAppliedSSN! ?? '-';
this._lastAppliedBackupInfoField.text.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename ?? '-';
this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : '-';
if (_isBlobMigration) {
if (!this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) {
this._currentRestoringFileInfoField.text.value = '-';
} else if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename === this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) {
this._currentRestoringFileInfoField.text.value = loc.ALL_BACKUPS_RESTORED;
} else {
this._currentRestoringFileInfoField.text.value = this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename;
}
}
if (this._shouldDisplayBackupFileTable()) {
await this._fileCount.updateCssStyles({
...styles.SECTION_HEADER_CSS,
display: 'inline'
});
await this._fileTable.updateCssStyles({
display: 'inline'
});
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
if (tableData.length === 0) {
await this._emptyTableFill.updateCssStyles({
'display': 'flex'
});
this._fileTable.height = '50px';
} else {
await this._emptyTableFill.updateCssStyles({
'display': 'none'
});
this._fileTable.height = '300px';
// Sorting files in descending order of backupStartTime
tableData.sort((file1, file2) => new Date(file1.backupStartTime) > new Date(file2.backupStartTime) ? - 1 : 1);
this._fileTable.data = tableData.map((row) => {
return [
row.fileName,
row.type,
row.status,
row.dataUploaded,
row.copyThroughput,
convertIsoTimeToLocalTime(row.backupStartTime).toLocaleString(),
row.firstLSN,
row.lastLSN
];
});
}
}
this._cutoverButton.enabled = false;
if (migrationStatusTextValue === MigrationStatus.InProgress) {
if (_isBlobMigration) {
if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
this._cutoverButton.enabled = true;
}
} else {
const restoredCount = this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.filter(
(a) => a.listOfBackupFiles[0].status === BackupFileInfoStatus.Restored)?.length ?? 0;
if (restoredCount > 0) {
this._cutoverButton.enabled = true;
}
}
}
this._cancelButton.enabled =
migrationStatusTextValue === MigrationStatus.Creating ||
migrationStatusTextValue === MigrationStatus.InProgress;
this._retryButton.enabled = canRetryMigration(migrationStatusTextValue);
} catch (e) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_STATUS_REFRESH_ERROR, e);
console.log(e);
} finally {
this.isRefreshing = false;
this._refreshLoader.loading = false;
}
}
private async createInfoField(label: string, value: string, defaultHidden: boolean = false, iconPath?: azdata.IconPath): Promise<{
flexContainer: azdata.FlexContainer,
text: azdata.TextComponent,
icon?: azdata.ImageComponent
}> {
const flexContainer = this._view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
'flex-direction': 'column',
'padding-right': '12px'
}
}).component();
if (defaultHidden) {
await flexContainer.updateCssStyles({
'display': 'none'
});
}
const labelComponent = this._view.modelBuilder.text().withProps({
value: label,
CSSStyles: {
...styles.LIGHT_LABEL_CSS,
'margin-bottom': '0',
}
}).component();
flexContainer.addItem(labelComponent);
const textComponent = this._view.modelBuilder.text().withProps({
value: value,
CSSStyles: {
...styles.BODY_CSS,
'margin': '4px 0 12px',
'width': '100%',
'overflow': 'visible',
'overflow-wrap': 'break-word'
}
}).component();
let iconComponent;
if (iconPath) {
iconComponent = this._view.modelBuilder.image().withProps({
iconPath: (iconPath === ' ') ? undefined : iconPath,
iconHeight: statusImageSize,
iconWidth: statusImageSize,
height: statusImageSize,
width: statusImageSize,
CSSStyles: {
'margin': '7px 3px 0 0',
'padding': '0'
}
}).component();
const iconTextComponent = this._view.modelBuilder.flexContainer()
.withItems([
iconComponent,
textComponent
]).withProps({
CSSStyles: {
'margin': '0',
'padding': '0'
},
display: 'inline-flex'
}).component();
flexContainer.addItem(iconTextComponent);
} else {
flexContainer.addItem(textComponent);
}
return {
flexContainer: flexContainer,
text: textComponent,
icon: iconComponent
};
}
private _shouldDisplayBackupFileTable(): boolean {
return !isBlobMigration(this._model._migration);
}
private _getMigrationStatus(): string {
return this._model.migrationStatus
? getMigrationStatus(this._model.migrationStatus)
: getMigrationStatus(this._model._migration);
}
}
interface ActiveBackupFileSchema {
fileName: string,
type: string,
status: string,
dataUploaded: string,
copyThroughput: string,
backupStartTime: string,
firstLSN: string,
lastLSN: string
}
interface InfoFieldSchema {
flexContainer: azdata.FlexContainer,
text: azdata.TextComponent,
icon?: azdata.ImageComponent
}

View File

@@ -7,53 +7,45 @@ import { DatabaseMigration, startMigrationCutover, stopMigration, BackupFileInfo
import { BackupFileInfoStatus, MigrationServiceContext } from '../../models/migrationLocalStorage';
import { logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../../telemtery';
import * as constants from '../../constants/strings';
import { EOL } from 'os';
import { getMigrationTargetType, getMigrationMode, isBlobMigration } from '../../constants/helper';
export class MigrationCutoverDialogModel {
public CutoverError?: Error;
public CancelMigrationError?: Error;
public migrationStatus!: DatabaseMigration;
constructor(
public _serviceConstext: MigrationServiceContext,
public _migration: DatabaseMigration
) {
}
public serviceConstext: MigrationServiceContext,
public migration: DatabaseMigration) { }
public async fetchStatus(): Promise<void> {
this.migrationStatus = await getMigrationDetails(
this._serviceConstext.azureAccount!,
this._serviceConstext.subscription!,
this._migration.id,
this._migration.properties?.migrationOperationId);
const migrationStatus = await getMigrationDetails(
this.serviceConstext.azureAccount!,
this.serviceConstext.subscription!,
this.migration.id,
this.migration.properties?.migrationOperationId);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.MigrationStatus,
{
'migrationStatus': this.migrationStatus.properties?.migrationStatus
},
{}
);
// Logging status to help debugging.
console.log(this.migrationStatus);
{ 'migrationStatus': migrationStatus.properties?.migrationStatus },
{});
this.migration = migrationStatus;
}
public async startCutover(): Promise<DatabaseMigration | undefined> {
try {
this.CutoverError = undefined;
if (this._migration) {
if (this.migration) {
const cutover = await startMigrationCutover(
this._serviceConstext.azureAccount!,
this._serviceConstext.subscription!,
this._migration!);
this.serviceConstext.azureAccount!,
this.serviceConstext.subscription!,
this.migration!);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.CutoverMigration,
{
...this.getTelemetryProps(this._serviceConstext, this._migration),
...this.getTelemetryProps(this.serviceConstext, this.migration),
'migrationEndTime': new Date().toString(),
},
{}
@@ -67,30 +59,21 @@ export class MigrationCutoverDialogModel {
return undefined!;
}
public async fetchErrors(): Promise<string> {
const errors = [];
await this.fetchStatus();
errors.push(this.migrationStatus.properties.migrationFailureError?.message);
return errors
.filter((e, i, arr) => e !== undefined && i === arr.indexOf(e))
.join(EOL);
}
public async cancelMigration(): Promise<void> {
try {
this.CancelMigrationError = undefined;
if (this.migrationStatus) {
if (this.migration) {
const cutoverStartTime = new Date().toString();
await stopMigration(
this._serviceConstext.azureAccount!,
this._serviceConstext.subscription!,
this.migrationStatus);
this.serviceConstext.azureAccount!,
this.serviceConstext.subscription!,
this.migration);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.CancelMigration,
{
...this.getTelemetryProps(this._serviceConstext, this._migration),
'migrationMode': getMigrationMode(this._migration),
...this.getTelemetryProps(this.serviceConstext, this.migration),
'migrationMode': getMigrationMode(this.migration),
'cutoverStartTime': cutoverStartTime,
},
{}
@@ -104,7 +87,7 @@ export class MigrationCutoverDialogModel {
}
public confirmCutoverStepsString(): string {
if (isBlobMigration(this.migrationStatus)) {
if (isBlobMigration(this.migration)) {
return `${constants.CUTOVER_HELP_STEP1}
${constants.CUTOVER_HELP_STEP2_BLOB_CONTAINER}
${constants.CUTOVER_HELP_STEP3_BLOB_CONTAINER}`;
@@ -116,16 +99,16 @@ export class MigrationCutoverDialogModel {
}
public getLastBackupFileRestoredName(): string | undefined {
return this.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename;
return this.migration.properties.migrationStatusDetails?.lastRestoredFilename;
}
public getPendingLogBackupsCount(): number | undefined {
return this.migrationStatus.properties.migrationStatusDetails?.pendingLogBackupsCount;
return this.migration.properties.migrationStatusDetails?.pendingLogBackupsCount;
}
public getPendingFiles(): BackupFileInfo[] {
const files: BackupFileInfo[] = [];
this.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach(abs => {
this.migration.properties.migrationStatusDetails?.activeBackupSets?.forEach(abs => {
abs.listOfBackupFiles.forEach(f => {
if (f.status !== BackupFileInfoStatus.Restored) {
files.push(f);

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