Compare commits

...

345 Commits

Author SHA1 Message Date
Karl Burtram
13e3627627 Update the path to the smoke test (#15139) 2021-04-14 16:51:10 -07:00
Aditya Bist
e2111fe493 Links handling in commands (#15118) (#15137)
* format

* update external options type

* add command flag for command

* Allow commands in Notebooks

Co-authored-by: chgagnon <chgagnon@microsoft.com>
(cherry picked from commit e151668c81)
2021-04-14 15:35:15 -07:00
Aditya Bist
7b46269b44 add flag for proxy (#15120) (#15126)
* add flag for proxy

* update distro hash

* Bump distro hash

* Bump distro

Co-authored-by: kburtram <karlb@microsoft.com>
Co-authored-by: chgagnon <chgagnon@microsoft.com>
(cherry picked from commit b6bdb68596)
2021-04-14 10:22:17 -07:00
Aditya Bist
69b9a19634 fixed css for font family (#15119) (#15124)
(cherry picked from commit 90868bc0ad)
2021-04-13 21:15:48 -07:00
Aditya Bist
ebe835ec99 Remote CLI fixes (#15117) (#15125)
* fixes from vscode

* update distro

* fix distro

* fix hygiene

* reorder hygiene

* Update distro

Co-authored-by: chgagnon <chgagnon@microsoft.com>
(cherry picked from commit 1b78008258)
2021-04-13 21:15:22 -07:00
Justin M
d4e25f4d89 Bumped version of Kusto to 0.5.3 for SqlToolService 92 (#15116) 2021-04-13 13:54:50 -07:00
Charles Gagnon
809d4de862 Only reveal parent node when finding notebook item (#15091) (#15103)
(cherry picked from commit 42cd650147)
2021-04-12 16:46:30 -07:00
Chris LaFreniere
e1aae951a3 Change to fspath from resource tostring (#15069) (#15078) 2021-04-09 14:21:29 -07:00
Sakshi Sharma
bdd99bd0d8 Fix deploy data to be presented on dashboard (#15073) (#15080)
* Fix deploy data to be presented on dashboard

* Bump sql-db-project extensions version

* Address comment
2021-04-09 13:26:56 -07:00
Barbara Valdez
c06bfad916 fix search loading animation (#15063) (#15066) 2021-04-08 16:45:06 -07:00
Maddy
1c91f7971e Select active notebook in viewlet when opened from command pallet (#15027) (#15065)
* await initialized

* fixes specific for windows

* address comments

* update comment
2021-04-08 16:44:08 -07:00
Barbara Valdez
8c9037fbdf fix anchor links in wysiwyg (#14950) (#15054)
* fix anchor links in wysiwyg
2021-04-08 12:51:20 -07:00
Udeesha Gautam
d029bb9602 Fixing an issue where backup launch was freezing ADS and restore launch was not doiing anything. This repros in absence of any connetcions. (#15041) (#15044) 2021-04-08 09:06:58 -07:00
Udeesha Gautam
f0558714a4 Fix for Bug #15020 dashboard update with multiproject (#15029) (#15042)
* Fix to enable two projects showing only their data even if opening together

* Fixing a typo

* Taking in PR comments
2021-04-07 18:37:42 -07:00
Charles Gagnon
49147305e8 Fix event property name (#15026) (#15031)
(cherry picked from commit 1c66499910)
2021-04-07 16:15:34 -07:00
Barbara Valdez
519012c690 bump sqlservernotebook version (#15025) (#15039) 2021-04-07 15:51:20 -07:00
Udeesha Gautam
ff05bc2b03 Add message when no history exists on projects dashboard (#15002) (#15019)
* Add message when no history exists on projects dashboard

* Bump version for sql db projects

* Update text, add refresh button

* Remove commented code

Co-authored-by: Sakshi Sharma <57200045+SakshiS-harma@users.noreply.github.com>
2021-04-07 13:22:00 -07:00
Charles Gagnon
b54beb6e7a Add required minimum version for azdata extension (#15010) (#15015)
* Add check for minimum required azdata version

* cleanup

* remove unused

* comment

* param comment

* Fix tests
2021-04-07 10:53:08 -07:00
Kim Santiago
dbdcc3d20a fix schema compare dropdown not selecting correct db when multiple active connections (#14999) (#15012)
* fix schema compare dropdown not selecting correct db when multiple active connections

* fix when no username so default is used
2021-04-06 19:34:55 -07:00
Justin M
cf48776710 Increased required version for Kusto from 1.27 to 1.28 (#15001) 2021-04-06 14:05:37 -07:00
Vasu Bhog
309f750b92 See and Edit Selected Links in Callout Dialog (#14987) (#15000)
* Add URL label to linkCallout

* add test for file link
2021-04-06 13:26:51 -07:00
Charles Gagnon
8c92af3016 [Port] Update minimum required azdata version (#14998)
* Update minimum required azdata version to latest release (#14995)

* Update minimum required azdata engine version - Arc (#14996)

* vBump Arc/Azdata

* undo vbump

* vBump
2021-04-06 11:25:26 -07:00
Charles Gagnon
9b117da9cb Sign vulkan-1 DLL (#14976) (#14983)
* Sign DLLs

* Only sign vulkan-1
2021-04-06 10:34:38 -07:00
Charles Gagnon
d12a7b81fd Port 43db30d1da (#14986)
Co-authored-by: Maddy <12754347+MaddyDev@users.noreply.github.com>
2021-04-06 10:13:35 -07:00
Alan Ren
3eb705e77b redraw table header (#14963) (#14988) 2021-04-05 19:49:07 -07:00
Charles Gagnon
b421b19b73 Arc updates for March release (#14970) (#14974)
* Updated Postgres Spec for where to find engine version, removed calling calling -ev in edit commands (#14735)

* Added spec.engine.version, took out calling engine version with edit calls

* Added text wrong place

* missed updates

* PR fix

* Update Arc Postgres troubleshooting notebook

Co-authored-by: Brian Bergeron <brberger@microsoft.com>

* Remove AzdataSession from azdata commands (#14856)

* remove session

* Add in controller-context support

* Revert "Add in controller-context support"

This reverts commit 3b39b968efbf6054041cb01cb2d8443532643a82.

* Add azdataContext to login

* Undo book change

* Undo change correctly

* Add controller context support (#14862)

* remove session

* Add in controller-context support

* Add params to fake

* Fix tests

* Add info and placeholder for controller URL/name (#14887)

* Add info and placeholder for controller URL

* add period + update name

* update memento and allow editing of namespace/URL

* vBump

* vBump

* Fix tests

Co-authored-by: nasc17 <69922333+nasc17@users.noreply.github.com>
Co-authored-by: Brian Bergeron <brian.e.bergeron@gmail.com>
Co-authored-by: Brian Bergeron <brberger@microsoft.com>

Co-authored-by: nasc17 <69922333+nasc17@users.noreply.github.com>
Co-authored-by: Brian Bergeron <brian.e.bergeron@gmail.com>
Co-authored-by: Brian Bergeron <brberger@microsoft.com>
2021-04-05 13:00:26 -07:00
Charles Gagnon
124f7ca887 Remove survey link (#14971) 2021-04-05 12:49:19 -07:00
Alan Ren
c2d2cf5a82 limit the data size for chart rendering (#14949)
* limit the rows feed to charts

* add telemetry and option to hide

* fix typo

* updates

* comments

* notebook fix
2021-04-02 19:36:10 -07:00
Maddy
13ad4c9497 Select notebook in viewlet when opened in editor (#14854)
* test changes

* code clean up

* remove extra line

* remove commented code

* remove unnecessary import

* update expand logic

* implement parent,children on bookitem

* merge conflicts

* refactor code

* fix trustBook test

* typo on condition

* feedback

* indexOf to include

* fix undefined error on expand

* remove set parent

* revert getTreeItem logic

* Fix duplicated nodes

* remove debug

* Clean up logic

* Fix tests

* Fix logic

* cleanup

* Cleanup findAndExpandParentNode (#14960)

Co-authored-by: chgagnon <chgagnon@microsoft.com>
2021-04-02 19:33:41 -07:00
Aasim Khan
684dfc9760 Surfacing migration errors in dashboard (#14956)
* vbumping migration

* Adding 2 new icons cancel and warning

* Fixed help link display text in assessments

* Adding summary page redesign and resource name validations

* Made headings bold

* Fixed sku recommendation page styling
Added check item for assessment

* Validating account dropdown after token refresh

* Renamed cutover to mode

* cutover to mode renaming changes.

* Converting to details api for more warnings

* Added target database name and fixed cancel icon

* Surfacing warning info in dashboard.

* Consolidated fetch migrations logic
Localilzed some strings
Surface migration errors in dashboard and status page
Table redesign in status dialog
Fixed a major bug that happens when multiple dashboards are opened due to class variable sharing

* removing console count

* Fixing regex for SQL MI database names

* Allowing spaces in regex
2021-04-02 18:49:34 -07:00
Barbara Valdez
fde5caa9a4 Add documentation and fix paths (#14948)
* use posix relative path

* add doc link in dialog

* rename book to Jupyter book
2021-04-02 15:44:25 -07:00
Chris LaFreniere
0d3112ef35 Adding Rendered Notebook Diff Option (#14860)
* First attempt nb diff preview

* First attempt nb diff preview

* Simplify everything

* Interim scroll one way

* Double scroll

* Add setting

* Add tests

* Add comment

* Fix tests

* first round PR comments

* Ensure scrollable portion has scrollbar in diff

* Fix sqllint errors, register events

* Fix scrolling, readonly
2021-04-02 14:49:52 -07:00
Charles Gagnon
e7be1daf5c Switch azuredatastudio-sqlite repo (#14940)
* Switch azuredatastudio-sqlite repo

* Bump version
2021-04-02 09:14:50 -07:00
Sakshi Sharma
30a2b76faf Add background image to sql db projects dashboard (#14942)
* Add background image to sql db projects dashboard

* Removed gradient color

* Added bottom border for header

* Fixed border color
2021-04-01 17:24:16 -07:00
Charles Gagnon
ce6ea8af41 Fix dropdown error & editor database dropdown validation (#14946)
* Fix dropdown error & editor database dropdown validation

* Set initial values

* Update comment

* hygiene

* remove unused

* Fix tests
2021-04-01 14:52:55 -07:00
Barbara Valdez
a00ffa0469 fix search action (#14937)
* fix search action

* remove book version from book tree item
2021-04-01 11:16:32 -07:00
Charles Gagnon
37af88e265 Fix some spelling errors (#14944) 2021-04-01 11:16:08 -07:00
Charles Gagnon
4c2969d4ca Don't log race condition error (#14922) 2021-04-01 11:11:43 -07:00
Charles Gagnon
97ce7b9b67 Use shared packages in markdown-language-features dependencies (#14936)
* Use shared webpack packages in markdown-language-features dependencies

* update yarn post merge

* Remove ts-loader too
2021-04-01 09:44:41 -07:00
Alan Ren
ef8396f783 fix query plan doc failed to load issue (#14943)
* fix query plan doc failed to load issue

* organize imports
2021-04-01 09:41:41 -07:00
Barbara Valdez
fc13fb2200 Book Fixes (#14931)
* change folder icon and fix book path

* change folder icon in dark mode

* add folder.svg under resources
2021-03-31 14:02:08 -07:00
nasc17
2164bc0bb9 Unit test for Postgres Connections Strings Page (#14905)
* Start writing test (2)

* How read property pairs

* Connection strings check

* Delete folder

* PR fixes
2021-03-31 13:21:44 -07:00
Charles Gagnon
5151f337bc Log full stack trace for editor opened errors (#14932)
* Log full stack trace for editor opened errors

* space

* Actually print name
2021-03-31 13:13:41 -07:00
nasc17
6ab9844909 Cleaning up PG Compute and Storage code and adding reset for Cores (#14919)
* Add reset ability to cores request and limit

* Remove not needed !

* Changed withProperties to with Props
2021-03-31 11:54:53 -07:00
dependabot[bot]
d6e0a679f7 Bump y18n from 4.0.0 to 4.0.1 in /build (#14902)
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-31 11:29:19 -07:00
dependabot[bot]
eb5405d116 Bump y18n from 4.0.0 to 4.0.1 in /extensions/markdown-language-features (#14910)
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-31 11:27:43 -07:00
Kim Santiago
0fa9689345 add data-workspace unit tests to test-extensions-unit scripts (#14923) 2021-03-31 11:19:03 -07:00
Justin M
1ce10cd3f3 Increased sqltoolservice version for Kusto to 90 and bumped Kusto version to 0.5.2 (#14927) 2021-03-31 11:15:49 -07:00
Alex Ma
f7b9ca775a Update for language packs, (#14912)
* update DE lang pack extension strings from previous refresh

* bump lang pack versions for DE

* update for spanish langpack

* update for french langpack

* update for italian pack

* update for japanese pack

* update for korean pack

* update for Brazilian Portuguese pack

* update for Russian pack

* Update for Simplified Chinese pack

* update for Traditional Chinese pack

* added updated language packs for german and spanish

* changed vscode version

* added french and italian packs

* changes made to main

* japanese language pack added

* added korean pack

* Added Portuguese pack

* added russian pack, also small change to pt pack

* simplified chinese pack

* added traditional chinese pack

* deleted old language packs

* restore sql.i18n.json files

* Main.i18n.json reverted

* Revert sql json files

* main.i18n.json file for German updated

* updates to language packs 1

* main json for italian updated.

* update to japanese main

* finished translations for vs and sql in main.

* added ADS exclusive extensions to package.json

* fixed markdown and seti translation ids

* German language pack fixed

* Removed make from ads-language-pack-de and fixed es language pack

* french language pack fixed

* fixed italian language pack and restored typescript-basics

* removed unnecessary readme strings and nsions.

* fixed japanese pack

* Korean language pack fixed

* Portuguese language pack fixed

* fixed russian language pack

* Simplified Chinese pack fixed

* traditional chinese fixed

* restored sqlservernotebook
2021-03-31 11:08:45 -07:00
Aasim Khan
e762f19815 Fixing migration private preview bugs WIP 3 (#14928)
* Adding server name to wizard and dialog title
Surfacing async errors
Fixing a bunch of strings to match the mockup

* Adding auto refresh for migration status

* Removing errors for sql vm migration

* using new logic to get sql server username

* Fixing help links

* Removing unncessary await
2021-03-31 10:19:59 -07:00
Enrique Mejia
00d2fadb7d Use {arg} to reference selected text in query shortcuts (#14894)
* Add selection parameter to query shortcuts

* Add description for selected text as parameter

* Modify feature description since it supports proceedures and queries

* Replace every {arg} inside query shortcut
2021-03-31 08:53:52 -07:00
Christopher Suh
cb619f55c7 mi page shows prompt to select DB to see any warnings (#14921) 2021-03-30 23:19:12 -07:00
Christopher Suh
28577baa87 Assessment Progress Page (#14909)
* wip assessment in progress page

* wip

* working assessment loader

* wip, radio cards not showing up

* working assessments in progress page

* cleanup

* bumped sqlmigration extension version

* cleanup
2021-03-30 23:18:54 -07:00
Barbara Valdez
8aa222d1c8 Add new file UI (#14773)
Adds a notebook or markdown file to a book's top level or a section by right click on Book Tree View
2021-03-30 18:44:52 -07:00
Sakshi Sharma
b774f09b6c Sql DB project dashboard (#14899)
* First set of changes for workspace dashboard implementing the toolbar

* Workspace dashboard container implementation (#14813)

* First set of changes for workspace dashboard implementing the toolbar (#14160)

* First set of changes for workspace dashboard implementing the toolbar

* Addressed comments

* Addressed one remaining comment

* Removed an extra comma in interfaces file

* Addressed comments

* Addressed comments

* Refactored a bit of code

* Remove unnecessary await

* Addressed comments

* First set of changes for workspace dashboard container

* Update targetPlatform icon+add Time column to deploy table

* Addressed comments

* Removed redundant class definition

* Addressed comments

* Addressed comments

* Change enum to union type in dataworkspace typings

* Fix tests

* Addressed comments
2021-03-30 17:37:53 -07:00
Charles Gagnon
4df77c73bf Fix slider component (#14918) 2021-03-30 17:33:59 -07:00
Kim Santiago
f4e1f85e0f Add dacpac references to sqlproj with relative path (#14877)
* relative paths written to sqlproj, but can't delete yet

* only keep track of relative path

* remove leading slash

* add test

* use path.relative

* Add error message if dacpac reference is located on a different drive
2021-03-30 17:06:04 -07:00
Kim Santiago
af4ad1fcb1 fix revealFileInOS command (#14913) 2021-03-30 15:11:48 -07:00
dependabot[bot]
d3f9277139 Bump y18n from 3.2.1 to 3.2.2 in /samples/sqlservices (#14911)
Bumps [y18n](https://github.com/yargs/y18n) from 3.2.1 to 3.2.2.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-30 14:51:09 -07:00
Justin M
7dfafe1393 dstsAuth Token Refresh (#14890)
* Refactored getToken to set tenantId based on providerId

* Changed logic to set tenantId when provider is not dstsAuth
2021-03-30 10:30:25 -07:00
Aasim Khan
a13e924a14 Fixing filters and removing not started migration tile (#14903) 2021-03-30 00:41:04 -07:00
Aasim Khan
a231d2aa82 Migration Private preview 1 fixes 2 (#14898)
* Removing canary host

* Rebranding extension name to Azure SQL Migration

* stopping instance table overflow in assessment dialog

* Added info message for details copied

* Limiting storage account and DMS to the same subscription as target

* making accounts page look like figma mockups

* converting error messages to warnings in cutover dialog

* making source config page look like figma mockups

* adding more filters for storage account and dms

* Adding validations for target database names.

* Fixing branding in other strings

* Adding types for SQL Managed Instance
2021-03-29 17:43:19 -07:00
Sakshi Sharma
69361b5c97 Update button texts for Create New project and Open existing project UIs (#14900) 2021-03-29 16:45:08 -07:00
Christopher Suh
896d0b0ea1 bump sts version (#14895) 2021-03-29 12:49:21 -07:00
Sakshi Sharma
bdbe4fb6de Fix telemetry for Sql db projects (#14896) 2021-03-29 12:20:56 -07:00
Alan Ren
4c5a4215f8 fix maximum call stack size reached issue (#14878)
* fix maximum call stack size reached issue

* Revert "fix maximum call stack size reached issue"

This reverts commit 178675633032a508ddb5585d1adc4f83bb243f55.

* add a few array operations

* use new push
2021-03-29 12:07:23 -07:00
Lucy Zhang
dd5adad772 Notebooks: fix save as csv/excel/json/xml (#14882)
* add getEncoding method to notebooks

* fix row 501 missing in serialization

* fix row index

* pr comment
2021-03-29 07:14:49 -07:00
Kim Santiago
ca19f08582 Add git clone option for opening existing workspace (#14828)
* added git option to dialog

* Add validation for cloning workspace and hide radio buttons for project

* add test

* cleanup
2021-03-26 15:30:29 -07:00
Lucy Zhang
bbee4a1f38 only restart server if there is an active session (#14885) 2021-03-26 15:04:08 -07:00
Kim Santiago
39a47b0053 Add telemetry for opening dacpac wizard (#14884)
* add telemetry for opening dacpac wizard

* add wizard open telemetry to core

* fix tests

* remove WizardOpen
2021-03-26 14:57:51 -07:00
nasc17
e080770c19 Updates how the Postgres Parameter Dashboard adds data to its table and discard values. (#14840)
* Clear updates in discard

* Update discarding values

* Changed where the info bubble is created , added function for collecting all changed components.

* Remove parameters if the same as original value  reset individually

* Moved refreshParametersTable to be private function, try catch for postgresmodel.refresh

* Discard enable if fail

* Made changedCompoenentValues a set, removed unnessaery ! throughout file, cleaned up connec and load
2021-03-26 13:59:33 -07:00
Aasim Khan
4d78aefe57 Fixing bugs for migration extension private preview 1. (#14872)
* Fixing Database backup page target layout

* Filtering out Azure sql db issues from assessment results
Correcting the database count for issued databases in sku rec page.

* Adding copy migration details button to migration status

* Adding start migration button to toolbar

* Fixing a syntax error in package.json

* Adding rg and location to target selection page
Filtering storage account by target location.

* Fixing dashboard title to azure sql migration

* Not making assessment targets selected by default.

* Adding tooltip for database and instance table items.

* Fixing duplicate task widget

* Some fixes mentioned in the PR
Localizing button text
renaming  a var
changing null to undefined.

* Adding enum for Migration target types

* Fixing a critical multi db migration bug because of unhandled race condition

* Adding Azure location api to azure core

* Adding source database info in status
2021-03-26 10:32:28 -07:00
nasc17
e0f24cc268 Add try catch block to model refresh in compute/storage pages (#14861)
* Add try catch block to model refresh in compute/storage pages

* spacing
2021-03-26 08:30:42 -07:00
Charles Gagnon
4b5aac57d3 Add return type to handler (#14876) 2021-03-25 17:12:58 -07:00
Charles Gagnon
5317d9ae0b Reload resource deployment types on extension changes (#14875)
* Reload resource deployment types on extension changes

* add comments
2021-03-25 16:41:36 -07:00
Charles Gagnon
b7e982e78a Add prompt to install required extensions for resource deployment (#14870) 2021-03-25 16:41:08 -07:00
Barbara Valdez
15f7b12849 Search smoke tests (#13469)
* Add smoke test

* add simple tests for searching in notebooks view

* address pr comments

* address pr comments

* add search smoke tests to separate file

* remove span from selector
2021-03-25 12:43:55 -07:00
Alan Ren
94c0795fc7 add 'start migration' action to dashboard toolbar (#14869)
* add 'start migration' action to dashboard toolbar

* localization
2021-03-25 12:36:37 -07:00
Alan Ren
5db6857c49 only return visible elements as focusable (#14864) 2021-03-25 09:49:16 -07:00
csigs
edac96a624 LEGO: check in for main to temporary branch. (#14858) 2021-03-25 09:33:17 -07:00
Aasim Khan
f61288d29e Fixing assessment UX for private preview (#14837)
* Fixing assessment UX for private preview

* localizing all the string

* Fixing comments

* Fixing dbname bug in assessment page.
2021-03-24 20:49:03 -07:00
Alan Ren
f12c8cd5d3 fix issue that user is not able to interact with checkbox in declarative table using keyboard (#14863)
* handle space key press

* correct fix
2021-03-24 19:07:12 -07:00
Alan Ren
2b7535f377 fire selection event when re-entered (#14857) 2021-03-24 16:40:20 -07:00
Lucy Zhang
f59e9b5695 Notebooks: Use new Python installation after configuration change (#14765)
* start new jupyter server

* restart session working (removed extra code)

* only restart server once

* shutdown session first then stop server

* add comments remove extra lines

* add comment

* fix test

* only restart jupyter sessions

* Dispose jupytersessionmanager and create new one

* move restart server logic out of notebookmodel

* move methods to azdata proposed

* pr comment
2021-03-24 15:31:20 -07:00
Leila Lali
130a439abc Removed 'preview' from MI (#14767) 2021-03-24 14:33:27 -07:00
Kim Santiago
dee5720676 make task history horizontally scrollable (#14846) 2021-03-24 13:41:02 -07:00
Alan Ren
92e9a423a0 declarative table fix (#14844)
* declarative table fixes

* reset selectedRow

* use eventHelper
2021-03-24 10:45:43 -07:00
csigs
0fb01b5b34 LEGO: check in for main to temporary branch. (#14852) 2021-03-24 10:08:22 -07:00
Alan Ren
b148b91a9c fix the layout issue (#14850) 2021-03-24 10:03:21 -07:00
csigs
689144a526 LEGO: check in for main to temporary branch. (#14842) 2021-03-24 09:37:28 -07:00
csigs
a3681dc0f9 LEGO: check in for main to temporary branch. (#14836) 2021-03-24 09:37:07 -07:00
csigs
4f255647ac LEGO: check in for main to temporary branch. (#14848) 2021-03-24 09:36:46 -07:00
Aasim Khan
e6a81d01cc Making source config more intiutive (#14849)
Adding loading component for assessment card
2021-03-24 00:49:33 -07:00
Alan Ren
7d94e247f6 card link styles (#14847) 2021-03-23 23:08:49 -07:00
Aasim Khan
de494f97ef bumping sts to get latest migration assessment api changes (#14843) 2021-03-23 21:06:25 -07:00
Christopher Suh
be9d18428f Deselect row everytime setProperties called (#14838)
* deselect row everytime setProperties called

* added data

* changed clear to only happen if data property changed
2021-03-23 15:40:21 -07:00
Alan Ren
6c54059f89 query results filtering and sorting (#14833)
* query results filtering and sorting (#14589)

* enable filter

* attach button style

* add hybrid data provider

* make filter and sort work

* fix editor switch issue

* configuration

* fix sort and filter

* add more specific selector

* fix hidden items issue

* update text

* revert text change

* fix copy results issue

* put feature behind preview flag

* comments

* fix tslint error
2021-03-23 11:30:41 -07:00
Barbara Valdez
5c67f3dbed fix tests on windows (#14827)
* fix tests on windows

* fix test
2021-03-23 10:03:48 -07:00
csigs
e2b6972ba9 LEGO: check in for main to temporary branch. (#14834) 2021-03-23 09:10:43 -07:00
csigs
f125e014fa LEGO: check in for main to temporary branch. (#14831) 2021-03-23 09:10:19 -07:00
csigs
8d0131b624 LEGO: check in for main to temporary branch. (#14826) 2021-03-23 09:09:54 -07:00
csigs
2d43cca3c8 LEGO: check in for main to temporary branch. (#14820) 2021-03-23 09:09:31 -07:00
csigs
0d951e1ef1 LEGO: check in for main to temporary branch. (#14815) 2021-03-23 09:09:19 -07:00
Aasim Khan
339d908d1d Adding windows auth support to sql-migration and misc bug fixes. (#14816)
* - Added coming soon message for learn more.
- Potential fix for learn more message

* Renaming of controller to sqlMigrationService

* Surfacing some errors
-Azure account is stale error
-Migration Service creation error.

* Adding refresh azure token validation.

* Fixing some errors pointed during PR
-Fixing property names
-Fixing count

* Fixing migration status
- Adding special error handling for resource not found error
- Deleting unfound migrations from local cache
- Using prefetched migration status for view all

Misc fixes:
- Using SQL server version name instead of number
- Fixing Icons on sku recommendation page
- Fixing table column width in cutover dialog
- Adding spinner button to refresh.

* Fixing all strings in migration service page and dialog

* fixed a string error in create service dialog

* Adding source config page to migration to support windows auth
Some refactorings for sqlDatabaseTree (still WIP)

* refactoring assessments code 1
introducing new interface for server assessments

* Filtering out non windows sql vms
Retaining selections made by user on assessments dialog

* Fix compile errors on sqlDatabaseTree

* Exposing migration status errors in cutover dialog

* Updating extension verion

* Correcting typos
Fixing compilation erros
Removing en-us from url
Fixing function names
Make UI calls unblocking

* Unblocking dialog in case of failed assessments
Localizing string
removing blocking code from UI
Fixing comments

* Fixed broken assessment page logic
2021-03-23 07:48:26 -07:00
Lucy Zhang
780ca84f9a Enable status bar for notebooks (#14817)
* enable status bar for notebooks

* add onCellExecutionStart event to notebook service

* fix test, change var name
2021-03-22 14:54:13 -07:00
Charles Gagnon
8fb54710fb Promote CssStyles to stable API (#14824)
* Promote CssStyles to stable API

* add comment

* Fix compile
2021-03-22 12:16:59 -07:00
Christopher Suh
1a74d0f3d4 bumped sqltoolsservice version (#14823) 2021-03-22 11:33:11 -07:00
Charles Gagnon
72295d46c2 Add close method to ModelView dashboards (#14812)
* Add close method to ModelView dashboards

* fix closing

* remove accessors

* Update errors
2021-03-22 10:17:16 -07:00
csigs
98ba49304e LEGO: check in for main to temporary branch. (#14802)
Co-authored-by: Alex Ma <alma1@microsoft.com>
2021-03-21 13:03:48 -07:00
Charles Gagnon
6ee321a719 Update sqlops-dataprotocolclient to 1.2.2 (#14807)
* Update sqlops-dataprotocolclient to 1.2.2

* Fix yarn.lock
2021-03-20 12:49:18 -07:00
Alan Ren
45fd89d6da radio button outline style (#14811) 2021-03-19 20:06:57 -07:00
Alan Ren
128d382c91 use label instead of handle for aria label (#14810) 2021-03-19 18:05:10 -07:00
nasc17
2086ca6039 Postgres Parameters edit calls missing try catch block (#14805)
* try catch block within save and reset functions

* pr fix

* Pr fix sessions

* Pr changes
2021-03-19 17:25:48 -07:00
Alan Ren
a5d2870344 make tenant list not tab focusable (#14809) 2021-03-19 17:03:30 -07:00
Kim Santiago
d658af153d fix publish profile with connection string ending in semicolon not loading (#14797) 2021-03-19 13:17:51 -07:00
Brian Bergeron
ce39b4bd19 Arc - Unit tests for deleting Postgres (#14502)
* tests for deleting postgres from overview page

* upgrade to azdata-test 1.5.0

* Fix api stubbing

Co-authored-by: Brian Bergeron <brberger@microsoft.com>
Co-authored-by: chgagnon <chgagnon@microsoft.com>
2021-03-19 13:12:26 -07:00
Alan Ren
69d593007e fix contrast issue of view container and view header (#14800)
* bring in vscode colors

* background color
2021-03-19 09:56:33 -07:00
csigs
8aa8cb2cc6 LEGO: check in for main to temporary branch. (#14779) 2021-03-19 09:14:34 -07:00
Kim Santiago
adc0f3e96d fix build errors in projectController.test.ts (#14798) 2021-03-18 18:31:53 -07:00
Kim Santiago
8068de5938 fix project tree tests failing on windows (#14759)
* fix project tree tests failing on windows

* add back test

* Addressing comments

* change to doc comment

* remove unnecessary change

* undo other change
2021-03-18 17:14:00 -07:00
Charles Gagnon
c65c856d2f Fix API issues for external extensions (#14796)
* Fix azdata API break

* Move dialogwidth to azdata.d.ts

* re-add parameter

* add comment back
2021-03-18 16:46:32 -07:00
Charles Gagnon
31ce58a8fc Cleanup telemetry keys (#14795)
* Add event for connection error

* Cleanup telemetry keys

* Fix missed keys
2021-03-18 15:52:57 -07:00
Lucy Zhang
af19f93b78 clean up sqlnotebookmanager (#14793) 2021-03-18 15:01:14 -07:00
Christopher Suh
172f428789 Fixed formatting/bugs (#14790)
* formatting changes & assessment title

* fixed width for description/impacted objects

* fixed formatting

* removed master, model, tempdb, and msdb

* fix warnings number

* used static variable
2021-03-18 14:33:17 -07:00
Charles Gagnon
a0a97d1611 Revert "Updated Postgres Spec for where to find engine version, removed calling calling -ev in edit commands (#14735)" (#14794)
This reverts commit 318559dcd7.
2021-03-18 14:19:59 -07:00
Charles Gagnon
11d4ea3232 Promote some proposed APIs to stable (#14788)
* Promote some proposed APIs to stable

* Fix spelling

(cherry picked from commit b88a9ba33e644f83a2b8deac9d99d86803605a85)
2021-03-18 13:11:43 -07:00
Barbara Valdez
5da4690be8 fix relative paths in wysiwyg (#14743)
* Convert relative paths to absolute paths based on the notebook uri before converting to markdown
2021-03-18 10:24:15 -07:00
Charles Gagnon
88b7960d01 Add Slider component (#14774)
* initial

* more cleanup

* update types
2021-03-18 09:47:36 -07:00
Zi Chen
03d2ac7206 Recursively delete folders in database projects (#14740)
* Recursively rmdir

* Remove comment

* Add test

* Add test assertions

* Comment

* Recursively rmdir

* Revert "Comment"

This reverts commit 287e7f824adfb2fcedb18ee222b34b8c6a5305b5.

* Re-commit Simplify Test
2021-03-18 09:20:41 -07:00
Charles Gagnon
07bd534453 Update replicas text (#14783) 2021-03-17 16:19:03 -07:00
nasc17
947633f244 Clear updates in discard (#14781) 2021-03-17 16:15:45 -07:00
Alan Ren
de91969b14 declarative table color (#14775) 2021-03-17 15:32:47 -07:00
Aasim Khan
c7cca7e9f0 Renaming controller to migration service and other bug fixes/ validations. (#14751)
* - Added coming soon message for learn more.
- Potential fix for learn more message

* Renaming of controller to sqlMigrationService

* Surfacing some errors
-Azure account is stale error
-Migration Service creation error.

* Adding refresh azure token validation.

* Fixing some errors pointed during PR
-Fixing property names
-Fixing count

* Fixing migration status
- Adding special error handling for resource not found error
- Deleting unfound migrations from local cache
- Using prefetched migration status for view all

Misc fixes:
- Using SQL server version name instead of number
- Fixing Icons on sku recommendation page
- Fixing table column width in cutover dialog
- Adding spinner button to refresh.

* Fixing all strings in migration service page and dialog

* fixed a string error in create service dialog
2021-03-17 14:55:24 -07:00
Christopher Suh
5917f869ef Update version number for next release (#14772) 2021-03-17 14:58:51 -04:00
Kim Santiago
f4d305ec16 vBump dacpac and sql database projects (#14768) 2021-03-17 11:14:48 -07:00
Christopher Suh
3d78fe02a4 Update CHANGELOG.md (#14766)
* Update CHANGELOG.md

* Update CHANGELOG.md
2021-03-17 13:51:15 -04:00
Christopher Suh
04d9b54c49 Updating README for March Release (#14752) 2021-03-17 13:42:26 -04:00
Jonny Johansson
a12f85dd02 Add missing "Data Direction"-option to line charts (#14764) 2021-03-17 10:24:28 -07:00
Charles Gagnon
7ffa7a9b23 Add support for replicas option when deploying MIAA (#14758) 2021-03-17 10:12:26 -07:00
Charles Gagnon
4425b2afd3 Fix MIAA connection strings page not populating (#14763) 2021-03-17 09:40:47 -07:00
Kim Santiago
a4fae7c1d8 fix database name edit box not announcing required (#14753) 2021-03-16 16:04:56 -07:00
Alan Ren
150dec7211 expose the resizable option for table column (#14757) 2021-03-16 15:45:25 -07:00
Charles Gagnon
89e5ee3c80 Fix BDC deployment removing all security settings from config (#14756) 2021-03-16 15:44:21 -07:00
Barbara Valdez
784d76b886 Remove book notebook from toc (#14704)
* Add remove notebook from book
2021-03-16 15:02:29 -07:00
Kim Santiago
ff766a8a14 update workspace opening popup to be modal (#14741) 2021-03-16 11:36:10 -07:00
Charles Gagnon
95fa61ee4e Activate extensions contributing deployment providers (#14750) 2021-03-16 11:11:13 -07:00
Charles Gagnon
0ecc053377 Fix blank MIAA/PG deployment wizard pages (#14714) 2021-03-16 10:05:47 -07:00
Charles Gagnon
97813b4c20 vBump arc (#14748) 2021-03-16 08:29:40 -07:00
nasc17
318559dcd7 Updated Postgres Spec for where to find engine version, removed calling calling -ev in edit commands (#14735)
* Added spec.engine.version, took out calling engine version with edit calls

* Added text wrong place

* missed updates

* PR fix
2021-03-16 08:25:16 -07:00
Charles Gagnon
7a4852b047 Add Product Feedback survey link to welcome page (#14705)
* Fix welcome page

* Add link
2021-03-15 16:23:56 -07:00
Drew Skwiers-Koballa
9beb7804e6 sqlproj - keep new object input box open when user navigates UI 2021-03-15 13:00:01 -07:00
Sakshi Sharma
0209b55ea2 Change no workspace content text (#14726)
* Change no workspace content text

* Address comment
2021-03-15 11:48:27 -07:00
Karl Burtram
05d88f4971 Fix script loading in sandbox windows (#14727)
* Fix script loading in sandbox windows

* Avoid uglify error
2021-03-15 11:41:41 -07:00
Justin M
4140d0da79 Bumped sqltoolservice version to 82 and kusto package version to 0.5.1 (#14718) 2021-03-15 09:40:52 -07:00
Charles Gagnon
b3e141a150 Update missed connection management return values (#14722) 2021-03-15 09:24:43 -07:00
Sakshi Sharma
9df7c6c1aa Added integration tests for schema compare (#14479)
* Added integration tests for schema compare

* Addressed comments

* Revert beforeEach change

* Addressed comments and reverted table creation change

* Removed cancelCompare test and addressed a comment

* Increase test timeout from 5 to 10mins
2021-03-14 11:27:16 -07:00
Alan Ren
7a9de9e708 vbump asde extension (#14720) 2021-03-12 17:26:25 -08:00
nasc17
42347b4681 Pass in empty string quotation (#14717) 2021-03-12 15:50:41 -08:00
Charles Gagnon
63828dcc15 Remove connectivity mode from Arc deployment (#14710) 2021-03-12 14:26:57 -08:00
nasc17
98828aa67d Unit test for Postgres Model (#14651)
* Figure out initial test

* First trys at refresh unit test

* refresh unit test completed

* Connection profile unit test, need to pass in false

* Engine settings start

* All passing

* Clean up

* Removed class, updated showdialog stubs, using sinon spy

* Removed comment
2021-03-12 14:02:55 -08:00
Aasim Khan
d6cd3270a8 Fixing azdata engine version for import (#14666) 2021-03-12 13:17:48 -08:00
Charles Gagnon
90a5d9d817 Fix filtered resource options persisting across deployments (#14702) 2021-03-12 13:08:13 -08:00
Charles Gagnon
138fa3feb6 Update azdata login namespace param (#14670) 2021-03-12 12:01:55 -08:00
Charles Gagnon
7ea09d3d17 Fix welcome page (#14701) 2021-03-12 11:17:19 -08:00
Alex Ma
8acc28e344 Update for extension XLFs (#14650)
* updates to extension XLFs

* revert dacpac

* dacpac updated
2021-03-12 10:55:23 -08:00
Alan Ren
f83925ce38 fix vm creation issue (#14691) 2021-03-12 10:53:02 -08:00
Charles Gagnon
b670ffc7e6 Update azdata login namespace param (#14670) 2021-03-12 10:48:32 -08:00
Aasim Khan
0277054b1f Vbump and vscode engine requirements update for SQL migration extension (#14665)
* Updated version and vscode engine requirements

* bumping the right engine

* changing vscode engine verison as *

* reverting version to 0.0.3
2021-03-11 22:37:17 -08:00
Charles Gagnon
1c671676bf Disable auto sync service (#14677)
* Disable auto sync service

* skip tests
2021-03-11 17:28:01 -08:00
Leila Lali
063953f743 Bug fixes for ML 0.8.0 (#14659)
* Bug fixes for ML 0.8.0
2021-03-11 14:49:28 -08:00
Charles Gagnon
bba3b93c6e Move BDC resource deployment type into BDC extension (#14657) 2021-03-11 10:33:21 -08:00
Charles Gagnon
cf862854c5 Update return types that can return undefined (#14660) 2021-03-11 10:09:50 -08:00
Charles Gagnon
6c877a4e20 Update commands and param names (#14658) 2021-03-11 07:29:19 -08:00
Aasim Khan
29c02e5746 Resetting subscriptions after AD tenant is changed in migration wizard (#14656)
* Resetting subscriptions after AD tenant is changed

* removing migration in progress info message at the end

* printing migration started errors in console.
2021-03-10 18:23:39 -08:00
Aasim Khan
50ac3b0fdf Fixing welcome page tour. (#14640)
* Fixing the icon names

* Fixing settings card position

* getting icon selectors from their source

* Changing var name from dataExplorer to dataExplorerId
2021-03-10 15:55:31 -08:00
Aditya Bist
46f805a8be fix a few more tags (#14648) 2021-03-10 15:10:08 -08:00
Charles Gagnon
e97429cb74 Bump arc version (#14653) 2021-03-10 15:03:14 -08:00
Kim Santiago
03a0d71486 Fix deploy upgrade existing not choosing the correct database (#14635)
* fix deploy upgrade existing not choosing the correct database

* fix test

* specify databaseDropdownValue as a string
2021-03-10 15:02:14 -08:00
Aasim Khan
ee2988f5fd Adding Azure AD tenant dropdown in migration wizard (#14637)
* Added dropdown to select azure tenant in accounts page

* Added dropdown label for tenant dropdown

* Moving deepcopy to  utils
Exporting tenant type from azurecore

* Removing unnecessary stylings

* removing unnecessary async
2021-03-10 12:48:44 -08:00
Christopher Suh
6389a5b0b0 Target Page Updates (#14581)
* fixed target page links

* wip

* wip

* update target page values

* wip

* fixed number of dbs

* add vm assessment dialog
2021-03-10 02:45:46 -05:00
Aasim Khan
bfa8ec0301 Adding database specific settings, cancel migration and other dark UI fixes (#14626)
* Made dashboard dark ui compat

* foundations for sql vm

* WIP

* Added cancel migration
Added refresh migraiton table
Added multi db config

* disabling cancel migration button if the migration is not in progress.

* Addressing some PR based coments
- Removing (s) from loc strings
- Adding return type to cancel migration
- removing _ from public vars
- localizing strings
- Adding name to dialogs for telemetry

* Adding todo comment for offline mode
2021-03-09 23:40:52 -08:00
Alan Ren
c05ba883c4 handle query gallery with tags (#14633)
* handle query gallery with tags

* fix typo
2021-03-09 19:19:48 -08:00
Charles Gagnon
c33116c625 Remove Online Services Settings option (#14624) 2021-03-09 17:20:07 -08:00
nasc17
973f151e07 Add a note that opening new support request from ADS works only after uploading configuration to the cloud/portal (#14601)
* Added note to support request page

* Fixed note
2021-03-09 15:09:21 -08:00
Barbara Valdez
4b462a41ab Queue concurrent calls when initializing contents in BookModel (#14586)
* Initial work for handling concurrent operations when calling initialized books

* fixes to init contents

* create a queue of deferred promises for initializing books

* resolve active promise and set to undefined

* remove duplicated variable

* address pr comments
2021-03-09 10:19:30 -08:00
Charles Gagnon
0906030aa2 Expose AzureAccount typings to other extensions (#14604) 2021-03-09 10:16:52 -08:00
Vladimir Chernov
46dd49d69a SqlAssessment azdata version update (#14613) 2021-03-09 10:09:11 -08:00
dependabot[bot]
a4297a2922 Bump elliptic from 6.5.3 to 6.5.4 (#14600)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-09 08:57:29 -08:00
dependabot[bot]
447e573911 Bump elliptic in /extensions/markdown-language-features (#14609)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-09 08:53:14 -08:00
Charles Gagnon
b5e66a715f Update connection event properties (#14608) 2021-03-09 08:39:14 -08:00
Cory Rivera
e2a5859155 Only include package versions in Manage Packages dialog if they're supported for the user's version of Python (#14584) 2021-03-08 18:05:10 -08:00
Charles Gagnon
bbdc324f17 Add PyZMQ third party license (#14607) 2021-03-08 17:04:26 -08:00
Jeff Trimmer
0bea923c0a Change tenant to tenant.id when passing to azdata.accounts.getAccountSecurityToken() method. (#14588) 2021-03-08 15:58:04 -08:00
Hale Rankin
809d35f173 Manage Packages icon position fix (#14528)
* Added CSS to treat manage packages icon position.

* Wired up a rough concept showing how I propose to pass a masked-icon class, along with the icon name - derived from the action icon path - to the injected action prior to rendering. The end result is an injected action with an icon which behaves like the others in the notebook toolbar.

* Revert "Added CSS to treat manage packages icon position."

This reverts commit 215a67244a78224fe8fd2e6480b7e50d37a15dc0.

* Revert "Wired up a rough concept showing how I propose to pass a masked-icon class, along with the icon name - derived from the action icon path - to the injected action prior to rendering. The end result is an injected action with an icon which behaves like the others in the notebook toolbar."

This reverts commit 1e0cf116602192aa554334d564e855167b0e8bb6.

* Modified LabeledMenuItemActionItem to add masked-icon class to injected label element.

* Modified LabeledMenuItemActionItem to add masked-icon class and styles for injected label element.

* To prevent conflict with Dashboard, I duplicated the existing LabelMenuItemActionItem and odified it.

* Added comment to new method. Added non-prefixed mask-image style.

* Renamed method and modified comments.
2021-03-08 15:34:49 -08:00
Alex Ma
16556b8316 Info prompt for results encoding when saving to JSON (#14533)
* encoding message prompt added.

* added don't show again prompt

* global scope added

* utc8 warning added.

* Remove extra space
2021-03-08 14:05:05 -08:00
Karl Burtram
9d1db372f0 Update package.json (#14597) 2021-03-08 10:29:05 -08:00
nasc17
dba5880f35 Postgres Resource Health Paage (#14575)
* Add podstatus to spec

* Added image to table and fixed spacing.

* Added pod status to spec

* PR fixes

* Added resource health page, created overiew box

* Pod condtion table is up

* Tryingt to fix how table refreshes

* Fixed how drop down changes table

* Overview box shows number of running and pending pods

* overview box refresh fix

* Updated summary section

* PR fixes

* Condensed create pod list function

* Added enum

* fixed refresh

* Fixed refresh, fixed if all availble section add
2021-03-08 10:05:11 -08:00
Kim Santiago
cecc83c89d Still load other projects even if one doesn't exist (#14585)
* still load other projects if one doesn't exist

* show all errors at the end

* update error message

* remove project(s)
2021-03-05 18:39:56 -08:00
Hale Rankin
972b649beb Connection error box style fix (#14469)
* Modified modal styles, limiting height of basic modal to 480px.

* wip - added new attachCalloutDialogStyler. Moved callout-specific styler code out of modal.ts

* Moved attach styler code to workbench/common. Added custom styles to imageCalloutDialog

* Moved styler code into calloutDialog. Added callout-specific theme colors to colorRegistry. Removed color styles from modal and callout stylesheets.

* Added CalloutDialogModal that extends CalloutDialog so that the callout can be instantiated from core. Revised calloutDialog so the position cn be passed in from where it is instantiated.

* Revised refactor of modal and image/link callouts so that callout dialog invoked by core can also use the styler. Removed unused properties from dialog code.

* Added conditional to dialogModal to use correct styler for callouts.

* Cleaned up styles. Modified custom colors.

* Wrapped call to positionCalloutDialog in conditional.

* Style, colors, styler and modal updates to align callout with latest OPAC toolkit styles.

* Moved calloutDialog stylesheet

* Consolidated styler code and added a flexible custom styler to provide values for dialogModal

* Added image callout code.

* Remove image callout dialog until wired fully

* Test fixes

Co-authored-by: chlafreniere <hichise@gmail.com>
2021-03-05 17:34:02 -08:00
Barbara Valdez
0ef99ab42a remove Jupyter string from books and notebooks in UI (#14583) 2021-03-05 16:26:34 -08:00
Lucy Zhang
f125b9b2c7 Notebooks: Fix callout dialog being cut off at the bottom of the document (#14579)
* add above position for callout dialog

* use if else
2021-03-05 15:23:44 -08:00
Drew Skwiers-Koballa
733c3628a1 drag n drop bounding characters (pgsql fix) (#14376)
* specifies object handling for all current providers

* implements the same for column dragndrop

* adjusted test

* adds pgsql provider name constant
2021-03-05 15:22:47 -08:00
Alan Ren
1d60287795 give more flexibility to the dialog width setting (#14570)
* give more flexibility to the dialog width setting

* one more place
2021-03-05 15:01:29 -08:00
Aasim Khan
63c59ed920 Adding support for sql vm and some target page UI fixes. (#14571)
* Added SQL VM dropdown
Fixed the icons in cards

* Added a better api for sql vms

* Making some PR related changes
1. Using map instead of foreach
2021-03-05 13:32:29 -08:00
Charles Gagnon
df06afa2ab Fix undefined error in telemetry service (#14577) 2021-03-05 12:46:03 -08:00
Vladimir Chernov
89c3207c94 sqlAssessment sync display fix (#14574)
* sqlAssessment sync display fix

* making append data Thenable<void>
2021-03-05 23:42:07 +03:00
Barbara Valdez
a17a4a585e [Editing Books] - Refactor buildToc method (#14532)
* refactor buildToc method to only modify the section found and return a boolean

* fix tests

* Address pr comments
2021-03-05 11:41:31 -08:00
Maddy
b82942a030 show find in notebook when opening from searchResults view (#14447)
* initial changes

* open notebook through our command

* address comments
2021-03-05 10:49:09 -08:00
Charles Gagnon
f6f45bc3f9 Add event for connection error (#14564) 2021-03-05 09:02:54 -08:00
Charles Gagnon
d2faf9075d Clean up dialog event hide reasons (#14566)
* Clean up dialog event hide reasons

* Remove done
2021-03-05 08:54:35 -08:00
Udeesha Gautam
21019f7452 updating sqltoolsservice in ADS to update dacfx (#14569) 2021-03-04 22:20:12 -08:00
Karl Burtram
1167e247af Fix hover tooltips (#14561) 2021-03-04 17:38:59 -08:00
Charles Gagnon
eef8f0e2ba Fix error from azdata tool upon startup (#14559)
* Fix error from azdata tool upon startup

* Update error
2021-03-04 17:05:54 -08:00
Charles Gagnon
4e1c7decde Add mocha dependencies (#14562) 2021-03-04 16:30:39 -08:00
Lucy Zhang
8e07a6f239 pass in pythonexecutable when getting packages (#14550) 2021-03-04 15:26:46 -08:00
Alan Ren
99d46917e8 remove refreshNode method (#14552)
* remove refreshNode method

* fix issue with finding required node handles
2021-03-04 15:15:15 -08:00
Kim Santiago
9cfba8e8e0 Reload ADS when data-workspace extension opens a workspace (#14540)
* reload ADS when workspace is entered

* move reloading so that reload also happens for file save workspace as commands

* fix build error
2021-03-04 13:40:13 -08:00
Chris LaFreniere
69a35b38b2 Callout Dialog Fixes + WYSIWYG Improvements for Insert Link (#14494)
* wip

* Works in all edit modes

* Default value set

* wip

* preventdefault

* cleanup, add tests

* markup -> markdown

* Ensure selection is persisted for WYSIWYG

* Add simple dialog tests and some PR feedback

* floating promise

* PR comments, formatted markdown refactor

* Change escaping logic + PR comments

* PR feedback
2021-03-04 12:51:13 -08:00
Vasu Bhog
0141db80bc [Notebook] Open Kernel Alias (Kusto) notebook with Kernel Alias (Kusto) as selected kernel (#14504)
* Open kernel alias notebook with kernel alias as selected kernel

* Add language info test

* fix test

Co-authored-by: Vasu Bhog <bhogvu@mail.uc.edu>
2021-03-04 12:36:25 -08:00
Alexander Ivanov
ccde5123fa Improve tree control sample. (#14549)
While working on the tree control for our extension, I noticed that the sample implementation was not optimal. For instance, `vscode.TreeDataProvider.onDidChangeTreeData` should only be called once for the top-most node that has changed, as all children will be refreshed as well.

This change updates the code of the sample to track state changes during propagation and only raise the event for the top-most parent node that has its state changing.

There also was an issue with root node `1` not rendering, which impacted the new algorithm, as it was not refreshing the tree, when invisible root was changing. In order to address this, I updated `getChildren` to actually return the `1` node, when root was requested. This in turn changed the presentation (`1` is now rendered in the tree) and addressed the problem with tree not refreshing when `1` is the node that raises the event.

Lastly, there is one more optimization that I didn't make due to the bug that is in the works: `getTreeItem` can actually return the `element` directly, without the copy, as it already implements `TreeComponentItem`. The problem currently is that `TreeNode` also defines an `id`, which causes an error during refresh. Once that is addressed - this part can be optimized as well.
2021-03-04 11:38:30 -08:00
Charles Gagnon
9c886dd80a Remove unneeded validation error messages (#14531) 2021-03-04 09:50:57 -08:00
Chris LaFreniere
8c04266ff4 Notebooks: Persist Chart Data when Re-Executing Cell (#14512)
* empty chart

* Update chart data appropriately

* Adding tests

* wip, cleanup

* PR feedback
2021-03-03 22:40:11 -08:00
Udeesha Gautam
8e2fd2ac84 update sqltoolsservice for dacfx (#14535)
* update sqltoolsservice for dacfx

* fixing wrong string
2021-03-03 20:10:46 -08:00
Lucy Zhang
74629c951d Notebooks: Fix pip installation not working (#14506)
* upgrade pip after python installation

* add comment

* only upgrade pip for new python installations

* reinstall pip version instead of upgrade to latest

* update comment

* get pip version through packages list
2021-03-03 17:44:45 -08:00
Charles Gagnon
2620d060dc Fix error when running Notebook integration tests (#14536) 2021-03-03 15:48:41 -08:00
Kim Santiago
42fba14d88 Fix how data workspace handles untitled workspaces (#14505)
* add more workspace apis

* update dialog and check workspace scheme

* cleanup

* add comment

* update create project from db dialog

* cleanup

* update names

* add test
2021-03-03 15:31:21 -08:00
Chris LaFreniere
6ecacd6faa wip (#14530) 2021-03-03 11:50:44 -08:00
Maddy
ae55da3c35 fix the check (#14529) 2021-03-03 11:28:12 -08:00
Kim Santiago
cea55560e8 fix a couple things in create project from db dialog (#14511) 2021-03-03 10:33:52 -08:00
Charles Gagnon
625c4520da Fix input box validation (#14508)
* Fix input box validation

* just use return from inputbox validate
2021-03-03 10:19:10 -08:00
Chris LaFreniere
56dcd4ba50 Only serialize attachments in JSON if exists (#14516) 2021-03-03 09:06:16 -08:00
Barbara Valdez
3f0ca8b714 Create book dialog improvements (#14429)
* add improvements TODO on creating book experience

* fix create book to support a more complex folder structure

* replace \\ to a forward slash on windows

* address pr comments

* fix tests

* use the posix version of path.sep
2021-03-02 21:23:28 -08:00
Alan Ren
36e228ebf7 fix description not provided issue (#14510) 2021-03-02 18:07:40 -08:00
Justin M
7940714d00 Updated Kusto version from 1.22 to 1.27 (#14513) 2021-03-02 17:51:14 -08:00
Aasim Khan
f2ae5419bb Adding migration status and cutover to extension (#14482) 2021-03-02 17:11:17 -08:00
nasc17
1e67388653 Reset scheduling parameters CPU/mem request/limit to defaults (#14492)
* reset scheduling parameters

* Added right quotation marks

* Fixed comment

* Worker text box needs to have value, can't pass in emptry string

* Fixed ConfigurationSpecModel and added doc comment to handleOnTextChanged

* Add to information bubbles that user can reset scheduling parameters by passing in empty value

* Changed name of handleOnTextChanged
2021-03-02 16:15:40 -08:00
Alan Ren
18bdb0f37d pass in notification service (#14499)
* pass in notification service

* make the parameter required
2021-03-02 13:39:31 -08:00
Hale Rankin
3c38e0cc8b Notebook cell and modal fix - follow up after Lucy's 14433. (#14457)
* Modal - added code to stop the keyboard event after it is handled. textCell and codeCell - onKey now fired on keydown.

* Moved stop method into the conditional with Escape key.
2021-03-02 12:11:57 -08:00
Charles Gagnon
8b34e034fc Bump azdata tool version (#14498) 2021-03-02 11:05:36 -08:00
nasc17
9e02123330 Add loading component to Postgres Connection String Page (#14468)
* Add loading component

* Removed setting loading to true in update command
2021-03-01 16:45:33 -08:00
nasc17
cf08963fc1 Node configuration needs to show different size for data and logs volume for postgresql. (#14466)
* Included storage size of log and data

* Added backups and removed putting 0

* Added localized constants
2021-03-01 16:45:05 -08:00
Eric Humphrey
7d53e1c185 Fix spelling of retrieval (#14490)
Changed retrival to retrieval.
2021-03-01 15:52:11 -08:00
Chris LaFreniere
9273572d5a Fix cell attachment types (#14485) 2021-03-01 15:06:12 -08:00
Charles Gagnon
ff508b1bf4 Lint typings (#14488) 2021-03-01 14:42:26 -08:00
Maddy
af3373bc4b check for isDirty false explicitly (#14427)
* check for isDirty false explicitly

* revert back test to not pass isDirty
2021-03-01 14:14:00 -08:00
Maddy
14b9628b52 fix: find highlight disappears on clicking the cell (#14342)
* update content only when changed

* maintain thr highlight on rerenders

* pr comments
2021-03-01 13:43:41 -08:00
Charles Gagnon
9432330c46 Update dependencies for url-parse (#14476)
* Update dependencies

* add vscode back in for git

* revert yarn.lock

* re-add notebook redirect

* Fix git tests

* Update other testrunner references

* Add testrunner

* Add mocha-multi-reporters
2021-03-01 09:28:03 -08:00
Chris LaFreniere
48a63e1f50 Notebooks: Add Support for Cell Attachment Images (#14449)
* Add to interfaces

* Works E2E

* Consolidate interface

* Add comments, cleanup

* Add some tests

* Cleanup

* interface cleanup

* Add more tests

* Add comments

* Add type for cell attachment

* wip
2021-02-28 16:40:41 -08:00
Charles Gagnon
8ce32215ba Add remote debugging port to launch scripts (#14472) 2021-02-26 18:00:10 -08:00
Charles Gagnon
5d07a3272e Add URI handler for resource deployment (#14470)
* Add URL handler for resource deployment

* Add tests
2021-02-26 15:50:55 -08:00
Charles Gagnon
ad045de1f0 Fix azdata tests (#14471)
* Fix azdata tests

* More fixes
2021-02-26 14:40:26 -08:00
nasc17
ac9a2dcf68 Nasc/per role parameters (#14425)
* Creating separate pages for worker and coordinator parameters

* Added new gear, hid coordinator server parameters page

* Commented out azdataApi

* Added white gear, created abstract class for server parameters page

* Chaged gear name, condensed base class more, changed file name

* Added colored gears for parameter pages

* Edited to describe configuring both coordinator and worker
2021-02-26 13:33:40 -08:00
nasc17
b10626d6f4 Nasc/per role compute and storage pg (#14424)
* Start adding coordinator section

* Coordinator section shown on CS page

* Updated information bubbles, edited discard button

* Cleaned up code, hid coordinator section

* Add return types to functions, fixed configuration string name

* Configuration section does not need to be edited when model is refreshed since it is not seen

* Changed back information bubble to inform that both worker and coordinator nodes will be configured
2021-02-26 13:17:53 -08:00
Justin M
c6c65000ca Added NoAuth authenticationType for Kusto (#14375)
* Added NoAuth authenticationType for Kusto

* Added SqlLogin to Kusto as an AuthType. Renamed NoAuth to None
2021-02-26 12:28:43 -08:00
Justin M
862d0d88fa Update Kusto Version and ToolService Version (#14428)
* Updated kusto toolservice version from 60 to 80

* Updated Kusto version
2021-02-26 11:10:05 -08:00
Charles Gagnon
b304fadfde Standardize SQL CARBON EDIT tag (#14465) 2021-02-26 10:34:32 -08:00
Alan Ren
a378a52b33 fix sorting issue in azure resource grid (#14459) 2021-02-26 10:30:48 -08:00
Charles Gagnon
013ecc4ddc Add support for initial values of Azure fields in resource deployment (#14458) 2021-02-26 09:33:34 -08:00
Lucy Zhang
ab114376aa fix python wizard behavior and enable smoke test (#14433) 2021-02-26 05:04:27 -08:00
Chris LaFreniere
dbc655a8f5 Notebooks: Ensure WYSIWYG Mode for Keyboard Shortcuts (#14416)
* Ensure WYSIWYG Mode for kb shortcuts

* Move logic down to cell model
2021-02-25 16:57:06 -08:00
Karl Burtram
e761eb12ef Revert "Update dependencies for url-parse (#14423)" (#14456)
* Revert "Update dependencies for url-parse (#14423)"

This reverts commit 71cbe3fbf0.

* Fix merge break
2021-02-25 16:00:47 -08:00
Karl Burtram
8db8027b5a Update readme for 1.26.1 release (#14451) 2021-02-25 14:50:15 -08:00
Hale Rankin
9e02cf86a4 calloutDialog refactor - new superclasses for insert image and insert link (#14385)
* calloutDialog refactor - split code specific to image and link into their own super classes. Moved callout styles into a new stylesheet.

* Image and Link inserts working.

* Stylesheets cleanup. Refactor cleanup.

* Removed CSS comment. Added missing image callout style. Revised generic open and cancel classes. Moved all remaining localized strings into shared constants file.
2021-02-25 14:44:25 -08:00
Kim Santiago
4053666bef bump sql database projects version and dependency (#14442) 2021-02-25 13:33:26 -08:00
Aditya Bist
2a125ee43b normalize sql carbon tag (#14445) 2021-02-25 13:32:12 -08:00
Alan Ren
b28e845506 fix multiple button issue (#14443) 2021-02-25 13:23:56 -08:00
Christopher Suh
dbb6b71908 Assessment page changes (#14415)
* assessment page changes

* code cleanup

* remove dead code

* fixed hardcoded value

* fix instance table bug

* Revert "fix instance table bug"

This reverts commit a924f44e64062a427c9fe4b12c0f368e78e6c04f.

* Revert "fixed hardcoded value"

This reverts commit 75661c457b6161b03c823d783fa9db97431c563f.
2021-02-25 16:05:09 -05:00
Charles Gagnon
afa2256467 Update remote yarn.lock (#14438) 2021-02-25 12:14:40 -08:00
Christopher Suh
f461d2aa14 Target Page Changes (#14421)
* target page changes

* remove SourceConfigurationPage

* fix build errors

* fixed build errors

* passing selected Dbs on to migration state model

* code cleanup

* fix build errors
2021-02-24 20:02:36 -05:00
Charles Gagnon
00feb955d9 Add telemetry for deployment type being selected (#14410)
* Add telemetry for deployment type being selected

* Fix build
2021-02-24 15:47:17 -08:00
Charles Gagnon
71cbe3fbf0 Update dependencies for url-parse (#14423)
* Remove/update dependencies

* switch to yarn.lock

* fix build errors

* Update min url-parse version
2021-02-24 15:39:10 -08:00
Barbara Valdez
3d5ff25d13 Fix debounce issue when book toc is updated (#14392)
* pass function to debounce

* remove debounce decorator and move watch methods to bookmodel

* Move the vscode tree change data event to book model

* address pr comments

* fix book tests
2021-02-24 14:24:50 -08:00
Alan Ren
48c456709e limit the digits after decimal point (#14422) 2021-02-24 13:07:37 -08:00
Charles Gagnon
4b40d66bca Add IconPath type (#14420)
* Add IconPath type

* Add ThemedIconPath subtype

* Update type
2021-02-24 12:49:54 -08:00
Charles Gagnon
587ac45418 Resource deployment clean up (#14418) 2021-02-24 12:11:06 -08:00
Christopher C
9d827869a1 Cavonac/create az db (#14132)
* visual consistent
added variable table

* typo

* update variables and code

* variable descriptions

* typos

* typo

* added next steps

Co-authored-by: Alex Ma <alma1@microsoft.com>
2021-02-24 09:26:23 -08:00
Chris LaFreniere
94f7b329d6 Remove CSS Class that doesn't exist (#14401) 2021-02-23 18:28:25 -08:00
Kim Santiago
c05cece683 Expose adding files and folders in sql database projects (#14391)
* expose addToProject in dataworkspace.d.ts

* remove changes in data workspace extension

* add sqldbproj.d.ts

* change list to be Uris instead of strings

* don't add files/folders if any don't exist

* fix test on windows
2021-02-23 18:15:38 -08:00
Charles Gagnon
d5385f66d3 Default to current controller when deploying from Arc dashboard (#14409) 2021-02-23 17:17:52 -08:00
Christopher C
0108da2a24 Cavonac/params (#14264)
* visual consistent
added variable table

* typo

* update variables and code

* variable descriptions

* typos

* typo

* added next steps

* identifying param cells

* updates to readme and removing orphaned notebooks

Co-authored-by: Alex Ma <alma1@microsoft.com>
2021-02-23 13:36:09 -08:00
Alan Ren
561242a0d9 implement styler for infobutton (#14396)
* implement styler for infobutton

* comments

* one more comment
2021-02-23 13:28:48 -08:00
Alex Ma
948bb5bc34 Finished Failover Clustering Notebook and preliminary Availability Group Notebook. (#14339)
* WIP changes

* toc.yml edit

* added first cell to AG page

* Changed to SSMS instructions, will need to change steps.

* switched back to SQL

* added clarification of primary server database requirement.

* fixed spaces

* split adding nodes to its own page.

* more changes

* added message for prerequisites step

* test with invoke command added

* added run direct command to failover cluster notebook

* test-cluster temp file added

* added module change and changed readme

* fixed domain name

* fixed escape

* Test Cluster changed to comment again due to permissions issue

* moved additional failover cluster to first notebook

* more cleanup

* more changes added

* added endpoint url

* fixed commas

* failover cluster wording changed

* added select for sqlvm name

* added a check for service account

* fixed secure password string

* first availability group command added

* Current Availability Group Page

* small change

* configure-ag change

* more changes

* changes made to notebooks
2021-02-23 10:41:39 -08:00
Alan Ren
e266bbd562 fix the dashboard toolbar not load issue (#14397) 2021-02-23 09:43:04 -08:00
Alan Ren
eb58cd74e7 add attach styler for modal dialog footer buttons (#14395) 2021-02-23 09:40:10 -08:00
Charles Gagnon
2e1fe9a266 Update MacOS icon (#14394)
* Update MacOS icon

* Update distro
2021-02-23 09:25:19 -08:00
Alex Ma
9148aa1ed5 update localization resource files (#14389) 2021-02-22 19:42:55 -08:00
Aasim Khan
d21ee4dc9e Added fetch logic for controllers (#14380)
* . Added fetch logic for controllers (no need to create a new one everytime)
. Fixed retention logic

* Fix field reloading logic.
Fixed localized string
Removing hardcoded colors
2021-02-22 19:00:39 -08:00
Aasim Khan
9daaa1c58b Fixing the release version in changelog (#14386)
* Fixing the release version in changelog

* Fixing the date for the Feb release 1.22
2021-02-22 13:31:55 -08:00
Barbara Valdez
5ece9b968a Create book UI (#14210)
* Add dialog for creating books

* create empty book

* add localized constants

* add validation to dialog

* reset the create book command to original

* address pr comments

* change error message

* Init book toc manager in bookTreeView
2021-02-22 13:29:09 -08:00
Kim Santiago
551eb76a42 filter data-workspace projects by ext (#14354)
* add filtering by project extension

* Fix tests

* addressing comments

* convert to lowercase
2021-02-22 13:21:25 -08:00
nasc17
f62889002d Adds copy button to subscription id on postgres properties page (#14374)
* add copy button to subscription id

* Changed ordering
2021-02-22 13:05:08 -08:00
Kim Santiago
9087a9fbb1 Show git status in projects tree (#14182)
* show git status in project tree

* fix for windows

* fix tests
2021-02-22 11:33:42 -08:00
nasc17
cd4024625f Fixed data controller name on postgres properties page (#14373)
* Fixed data controller name

* Took out extra textkeyvalue for datacontroller
2021-02-22 10:57:17 -08:00
Aasim Khan
138d3f97ba Updating changelog and readme for Feb 2021 release. (#14323)
* Adding changelog for Feb 2021 release.

* Fixed highlights

* Adding readme changes.
2021-02-22 10:05:43 -08:00
dependabot[bot]
18b93cf212 Bump acorn from 6.0.7 to 6.4.2 (#14347)
Bumps [acorn](https://github.com/acornjs/acorn) from 6.0.7 to 6.4.2.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/6.0.7...6.4.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-22 09:46:44 -08:00
Alan Ren
289dd3ba65 fix a few issues (#14378) 2021-02-21 17:58:39 -08:00
rajeshka
6ca777ad31 Increased the chartjs version to latest 2.9.4 (#14227)
* Increased the chartjs version to latest 2.9.4

* Update remote package.json

* updated remote-web-package.json

* Updates yard lock for remote-web-package.json
2021-02-19 15:49:15 -08:00
Alan Ren
83da03a728 selected cell summary for status bar (#14358) 2021-02-19 11:29:04 -08:00
Chris LaFreniere
f528ffea9b Hide Open in Editor for notebook search (#14356)
* Hide Open in Editor for notebook search

* Add type to new parameter
2021-02-19 11:05:02 -08:00
Aasim Khan
eb61af2f12 Updating prose import to fix double backslash bug (#14357) 2021-02-19 10:58:46 -08:00
Lucy Zhang
8727a8e97a disable nb smoke tests (#14365) 2021-02-19 10:27:17 -08:00
Christopher Suh
fcd099dd64 Bumped turndown version (#14343)
* bumped turndown

* fix WYSIWYG test

* remove comment

Co-authored-by: Vasu Bhog <vabhog@microsoft.com>
2021-02-18 20:36:32 -05:00
v-bbrady
ce18341284 Updates welcome page responsiveness (#13913)
* updates copy

* localizes copy

* updates welcome page UI responsiveness at direction from designers

* adjusts layout

* updates pr according to review comments

* localizes copy on welcome page
2021-02-18 10:52:33 -08:00
Justin M
aff9adf730 Add dstsAuth as option for saved credential in connectionStore.ts (#14332) 2021-02-18 10:30:02 -08:00
Aasim Khan
b719099ad3 First check in for Migration Dashboard (#14309)
* Adding Dashboard
Fixing auth keys api
create status Dialog

* making some changes requested in the PR

* switched to text component from dom component

* Adding TODO comment

* Fixing image url to work on windows

* Fixing stuff pointed out in PR comments

* adding return type to dasboard register function

* Adding more todos
2021-02-18 10:25:52 -08:00
Alan Ren
30f55be67d fix the header filter plugin issue (#14333) 2021-02-18 00:10:41 -08:00
Maddy
87a9ae0975 fix for parameterized notebook closure. (#14324)
* Two options

* remove commented line and add the null check

* fix tests: pass isDirty explicitly

* remove commented line

Co-authored-by: chlafreniere <hichise@gmail.com>
2021-02-17 19:16:31 -08:00
Kim Santiago
272c20bb38 bump versions for data developer extensions (#14327) 2021-02-17 15:38:06 -08:00
Charles Gagnon
d159a1eb50 Remove DOM component (#14328)
* Remove DOM component

* fix compile

* Remove enum type

* one more
2021-02-17 15:16:42 -08:00
Hale Rankin
05f97411fa New feature for Notebooks - Callout (#13078)
* New feature for Notebooks - callout. This utilizes a modifed modal.ts that renders a small modal set to appear at the trigger event and disappear when user clicks off of it. This is intended only for micro-interactions such as inserting links, tables and images into Notebooks. Error dialogs dependent on modal have been updated to implement the modified width property correctlty.

* Modified dialogStyle names in interface. Applied updates to files dependent on this. Wired up disposable listener for browsing local files in image select callout. Renamed callout to calloutDialog.

* Converted double quotes in CSS to single quotes. Revised run method, removing the promise wrapping from the conditional.

* Passing click target to modal for positioning. Created custom buttons and click event handlers for Insert image and Insert link.  Set feature behind preview flag.

* Revised structure and styles.

* Updated component for use with ML extension. Updated ML extension code with changes for custom dialog creation.

* Fixed async context for handleBrowse. Passing the new modal properties into Dialog implementation for ML.

* Added option to suppress callout dialog header and footer from the machine learning view file. Added a compact variant of callout. Corrected Dialog tab initialization.

* Maddy/callout modifications (#13586)

* initial changes

* canSelectFolders false

* change label, placeholder text on radiobtn click

* Added support for custom XY offset to account for modal size and to provide for fine-tuning of unique modal instances.

* Updated Image icon with latest from Design.

* Replaced node process with IPathService for retreiving image file.

* Added theme color lookups to provide default colors when none are provided.

* Added async/await to tests calling transformText.

* textCell, modal - swapped out HostListener for a member method which listens for the Esc keyup. Updated templates with method call on keyup. This cleaned up the double event call whenever the Esc key was pressed. Added property to modal Cancel button call to make it a secondary button.

* Cleaned up callout styles.

* Removed color use for input fields because component code delivers theme-specific values.

* Added check for CSS class `mac` on the body tag. Added conditionals to supply alternate dialogXYOffset in the case of non-Mac environment.

* Cleanup: Simplified DialogPosition. Renamed telemtry references and dialog related methods. Corrected spelling. Added missing signatures and types. Updated warning callout link URLs.

* Removed async and await as there are no promise dependencies. Added a signature.

* Revert "Fix windows insiders icons (#13579)" (#13630)

This reverts commit a0ef594792.

* Changed cores validation message (#13617)

* Changed cores validation message

* Missed validation

* Remove cores validation message

* Applied verification for cores change to miaa c+s page

* WYSIWYG Improvements to highlight (#13032)

* Improvements to highlight

* wip

* Tests pass

* Leverage escaping mechanism

* Tweak highlight logic

* PR comments

* add await to thenable method (#13635)

* Delete ConnectionDialogue.ipynb (#13634)

this nb was an attempt at creating a connection dialog. removing
not found in toc

* Added engine version argument to edit command.  (#13610)

* Added engine version argument to edit command. Neccessary for not using pg12

* Included for changing password in overview page

* Updated fakeazdataapi test

* Fix empty column issue (#13641)

Co-authored-by: Monica Gupta <mogupt@microsoft.com>

* add right padding to notebook toolbar action item (#13640)

* add right padding to action item

* remove extra line and add space

* Adding SQL Edge project template (#13558)

* Checkpoint

* removing flag for not creating subfolder

* Adding Edge template

* Removing janky map function

* Adding templates for additional objects

* Updating tests, fixing bug

* Added Edge project icon

* Updating strings to Drew-approved text

* Cleaning up template scripts and Edge project template names

* Update package.json (#13626)

* Update Import UI to match other UIs (#13637)

* Update Import UI to match other UIs

* Fixed another bug

* Fix notebook unordered grid values after papermill execution (#13614)

* Fix unordered table

* check entire first row schema:

* SQL Notebooks should not get affected

* delete unused variable and edit comments

* refactor for efficient table ordering

* nit naming

* Normalize path to change (#13660)

* vbump asde deployment extension (#13662)

do a patch version update, will adjust if the next change is a major one.

* Add test for dynamic enablement (#13602)

* Add test for dynamic enablement

* update names

* Remove debug console log (#13669)

* Remove placeholder on deployment wizards when field is disabled dynamically (#13658)

* Bump highlight.js in /extensions/markdown-language-features (#13675)

Bumps [highlight.js](https://github.com/highlightjs/highlight.js) from 9.15.10 to 10.4.1.
- [Release notes](https://github.com/highlightjs/highlight.js/releases)
- [Changelog](https://github.com/highlightjs/highlight.js/blob/master/CHANGES.md)
- [Commits](https://github.com/highlightjs/highlight.js/compare/9.15.10...10.4.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* add ability to control the enabled state of checkbox cells (#13644)

* control enabled state of checkbox cells

* add more check

* Prevent Table from Disappearing due to exception when looking for tHead (#13680)

* Prevent exception when tHead doesn't exist at node

* Add test for no thead

* Fix issue with pasting results in Teams (#13673)

* Fix issue with pasting results in Teams

* Addressed comment to change header tag to th

Co-authored-by: Monica Gupta <mogupt@microsoft.com>

* Add descriptions and validation to connected mode (#13676)

* Add dependent field provider to resource deployment (#13664)

* Add dependent field provider to resource deployment

* Change name to value provider service

* Add error handling

* providerId -> id

* Set dropdown value correctly

* missed id

* back to providerId

* fix updating missed id

* remove placeholder

* Have resource deployment providers return disposables (#13690)

* Add dependent field provider to resource deployment

* Change name to value provider service

* Add error handling

* providerId -> id

* Set dropdown value correctly

* missed id

* back to providerId

* fix updating missed id

* Make resource deployment providers disposable

* Retry publish and always try adding asset (#13700)

* Retry publish and always try adding asset

* Undo asset upload change

* Add logging

* Notebooks: Remove result set summary from saved metadata (#13616)

* remove result set summary from metadata

* remove batchId and id from celloutputmetadata

* remove extra line

* Add scan suppressions (#13705)

* Add action for responding to Needs Logs label (#13707)

* Fix action name (#13708)

* Add action for responding to Needs Logs label

* Fix action name

* Rename action config file (#13709)

* Add action for responding to Needs Logs label

* Fix action name

* Rename config file

* remove quotes

* Adding unit tests for schema compare service (#13642)

* Retry getConfig (#13712)

* Retry getConfig

* Add logging

* vBump Arc and Azdata (#13717)

* switch schema compare to use inputbox instead of table headers (#13715)

* Added Accounts and Database Backup Page to Migration wizard (#13548)

* Added localized strings
Created a db backup page
added radio buttons

* created components for database backup page

* Added account selection page

* Added accounts page

* Some more work done

- Added page validations
- Almost done with db backup except for a few api calls.

* Some more progress
added graph api for storage account

* Finished hooking up all the endpoints on db page.

* Some code fixed and refactoring

* Fixed a ton of validation bugs

* Added common localized strings to the constants file

* some code cleanup

* changed method name to makeHttpGetRequest

* change http result class name

* Added return types and return values to the functions

* removed void returns

* Added more return types and values

* Storing accounts in the map with ids as key
Fixed a bug in case of no subscriptions found

* cleaning up the code

* Fixed localized strings

* Added comments to get request api
Added validation logic to database backup page
removed unnecessary page validations.

* Added some get resource functions in azure core

* Changed thenable to promise

* Added arm calls for file shares and blob storage

* Added field specific validation error message

* Added examples in validation error message.

* Fixed some typings and localized string

* Added live validations to dropdowns

* Fixed method name to getSQLVMservers

* Use console.log for retry logging (#13722)

* Fixed Schema compare integration tests by adding retry (#13649)

* Add workspace information in Import UI (#13648)

* Add workspace information in Import UI

* Addressed comments

* Reduced space between Workspace heading and the label

* Lint azdata.d.ts (#13728)

* Added developer name to the list of developers. (#13725)

onboarding commit: Added developer name to the list.

* Fix environment variables for controller create (#13732)

* vbump schema compare and sql database projects (#13730)

* December release readme (#13733)

* Change server group look (#13608)

* change server group look

* remove dead code

* add top padding

* add bot padding as well

* fix heights to account for padding

* fix arrow alignment

* fix ellipses and node length parity

* fix alignment

* Make loading components not valid and improve RD radio group (#13738)

* Revert "Added Accounts and Database Backup Page to Migration wizard (#13548)" (#13742)

This reverts commit e169005571.

* Add loading text properties for option sources (#13743)

* Add loading text to deployment radio options

* Fix loading race condition

* Update text

* Add kube config and kube cluster to arc data controller screens (#13551)

* Un-skip and fix a few of the db projects tests (#13726)

* Un-skip and fix a few of the db projects tests

* Addressed comments

* Fix one test failure on Linux/Mac

* Update STS to revert SqlClient update (#13758)

* Update required azdata versions (#13762)

* fix the recent list (#13770)

* Adding base classes for data dev extension telemetry (#13763)

* adding telemetry dependencies for data-workspaces and sql-database-projects

* Adding telemetry dependencies for dacpac extension

* Adding telemetry base to data workspaces and projects

* Adding telemetry base code to the dacpac extension

Co-authored-by: Benjin Dubishar <benjin@Largo.local>
Co-authored-by: Sai Avishkar Sreerama <ssreerama@microsoft.com>

* Update changelog (#13773)

* Notebook Extension: First logging improvements (#13729)

* First logging improvements

* PR feedback for err output

* Add BEGIN/END to snippet (#13784)

* Adding database backup and accounts page to migration wizard (#13764)

* Added localized strings
Created a db backup page
added radio buttons

* created components for database backup page

* Added account selection page

* Added accounts page

* Some more work done

- Added page validations
- Almost done with db backup except for a few api calls.

* Some more progress
added graph api for storage account

* Finished hooking up all the endpoints on db page.

* Some code fixed and refactoring

* Fixed a ton of validation bugs

* Added common localized strings to the constants file

* some code cleanup

* changed method name to makeHttpGetRequest

* change http result class name

* Added return types and return values to the functions

* removed void returns

* Added more return types and values

* Storing accounts in the map with ids as key
Fixed a bug in case of no subscriptions found

* cleaning up the code

* Fixed localized strings

* Added comments to get request api
Added validation logic to database backup page
removed unnecessary page validations.

* Added some get resource functions in azure core

* Changed thenable to promise

* Added arm calls for file shares and blob storage

* Added field specific validation error message

* Added examples in validation error message.

* Fixed some typings and localized string

* Added live validations to dropdowns

* Fixed method name to getSQLVMservers

* Using older storage package

* Update typings/namings (#13767)

* Update typings

* more typings fixes

* switched fileshares and blobcontainers api to http requests

* removed the extra line

* Adding resource graph documentation link as a comment

* remove makeHttpRequest api from azurecore

Co-authored-by: Charles Gagnon <chgagnon@microsoft.com>

* change afterClean to beforeBuild for removing the generated assests.json (#13736)

* change afterClean to beforeBuild for removing the generated assests.json

* fix merge conflict

* rename files

* Clean up Loading Component typings (#13785)

* Clean up Loading Component typings

* add properties to impl

* Log active element when notebook smoke test fails (#13724)

* add error log for active element

* fix active element selector

* await isActiveElement call

* call waitforactiveleement

* Bump ini from 1.3.5 to 1.3.7 in /samples/sqlservices (#13776)

Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump ini from 1.3.5 to 1.3.7 in /build (#13765)

Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump ini from 1.3.4 to 1.3.8 (#13792)

Bumps [ini](https://github.com/isaacs/ini) from 1.3.4 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.4...v1.3.8)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump ini from 1.3.5 to 1.3.8 in /extensions/markdown-language-features (#13791)

Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* renable kernels dropdown test (#13727)

Verify no skipped core tests

* Remove redundant parameter in test scripts (#13755)

* Fire onDidSelect event when selecting event from code (#13691)

* Fire onDidSelect event when selecting event from code

* Fix import tests

* fix typo

* Use correct Azure graph endpoint & cleanup (#13786)

* Use correct Azure graph endpoint

* Add enum

* Update CODEOWNERS (#13798)

* Make project workspace selectable if no workspace is open yet (#13508)

* allow new workspace location to be editable

* fix workspace inputbox not showing up after toggling open workspace radio buttons

* add a few tests

* cleanup

* fix errors

* addressing comments

* fix filter for windows

* add error message if existing workspace file is selected and change picker to be folder only

* address comments

* fix typos and update tests

* vbump 2018 -> 2019 (#13800)

* Fix "not externalized correctly" warnings (#13806)

* Notebook Deep Link to Section (#13795)

* Notebook deep link to section

* fragment wip

* table component improvement (#13801)

* hyperlink column

* fixed width for image only button - old behavior

* Server Reports extension: fix for start and stop xevent sessions (#13565)

* fix for start and stop xevent sessions

* vscode.open for help URL

* setup info messages for localization

@kburtram - I could use an assist on updating the v# and publishing the vsix, but there's no rush this can wait until after the holidays

* Arc - Update Postgres name length limit (#13807)

Arc - Update Postgres name length limit. It was recently reduced from 12 to 11.

* Fix whitespace differences in sqlproj (#13805)

* add whiteSpaceAtEndOfSelfclosingTag

* update test baselines

* Fix paths for tests (#13816)

* Fixed the stray validation error message in Resource Deployment wizard (#13747)

* Fixed the stray validation error message

* Removed not working ' with validation
Adding back cancel button disabling

* mark a couple data workspace tests as unstable (#13822)

* cosmetic changes (#13820)

* cosmetic changes

* moved limitLongName function to the utils

* add . as trigger character (#13811)

* Remove hardcoded search box height (#13823)

* Use azdata-test modelview stubs (#13818)

* Filter vscode delegate command events (#13832)

* Fix duplicate SVG rendering (#13828)

* Fix select box event ordering (#13831)

* Fix select box event ordering

* more fixes

* Fix page

* Revert typing change

* Undo param

* Fix compile error

* Completely remove typings

* Dacpac - Showing error message to user if operation fails (#13830)

* Showing error message to user if operation fails.

* Added more tests

* Fix unstable data-workspace tests (#13824)

* stub file existing validation

* add error message

* change back to calling dialog.validate()

* move tests to separate dialogbase file and add more error message validation

* comment out the unstable unit test step (#13834)

* remove --build flag

* comment out unstable test

* add build tag back

* Removed padding-top / bottom declarations for text cell notebook-preview. This compacts the text cell by 14px on top and bottom. (#13815)

* Stop forcing left text align on tables (#13840)

* Have same connection logic for all nb int tests (#13844)

* Added fix for the infinite page refresh in resource Deployment tools page (#13813)

* Added fixed for the infinite loop in resource Deployment tools page
Generating events for select boxes only when the select box value is changed.

* Fixed the check logic in select method

* Reverted to old code and fixed some bugs

* Fixed event generation check logic

* Make new workspace inputbox editable in Create project from database dialog (#13842)

* update create project from database dialog to have editable new workspace

* add validation

* add test

* add error message

* Remove test for now

* cleanup

* add periods

* throw errors

* change return type to void

* Removal of Components folder, moving ADP notebook to its own notebook. (#13848)

* adp folder removed, notebook moved.

* moved ADP notebook to own folder.

* Passing click target to modal for positioning. Created custom buttons and click event handlers for Insert image and Insert link.  Set feature behind preview flag.

* Revert "Revert "Fix windows insiders icons (#13579)" (#13630)"

This reverts commit 111dcb4c6885d9ab1a24398b13f103835789e6e0.

* Corrected button style and declared dialogStyle for ManageModelsDialog. This fixes broken layout see when user clicks: Import or view models.

* Revised suppressHeader/Footer to renderHeader/Footer and fixed logic. Code cleanup. Changed how calloutType: IMAGE and LINK are implemented. Added comments where neeed.

* Fixed callout separator: Swapped out transparent for notebookToolbarLines

* Removed promise context from image callout logic.

* Moved calloutDialog into modal folder.

* Code and style adjustments per feedback. Removed dependency on mac body class. Cleaned up dialog theme. Revsied modal callout logic.

* Corrected CSS for notebook toolbar. Removed unused code. Added code to ensure that renderFooter would occur whether true or undefined.

* Code cleanup. Clarified more details in my code comments. Added comments to values that needed identification. Escaping user-supplied text before it is rendered.

* Renamed TriggerProperties interface and implementations to DialogProperties. Added default value for DialogStyle to sqlExtHost.api.impl so existing dialogs take flyout by default.

* Replaced null value with undefined. Revised theme logic to account for undefined color value.

* Fixed top 30px offset rule so callout dialogs do not get this added.

* Revised undefined check for modal theme color. Removed calloutCompact. Moved callout dialog widths into DialogWidth property when calling methods to create callout or dialog. Added comments. Revised CSS.

* Providing fallback value in the case of null. Hex: FFFFFF00 is white with no transparency.

* Removed fallback hex color for foreground. Added check for foregroundRgb before attempting to grab rgba values. Added footer top border color.

* Added formatting after resolving conflicts.

* Corrected implementation after taking changes from main.

Co-authored-by: Maddy <12754347+MaddyDev@users.noreply.github.com>
Co-authored-by: Charles Gagnon <chgagnon@microsoft.com>
Co-authored-by: nasc17 <69922333+nasc17@users.noreply.github.com>
Co-authored-by: Chris LaFreniere <40371649+chlafreniere@users.noreply.github.com>
Co-authored-by: Barbara Valdez <34872381+barbaravaldez@users.noreply.github.com>
Co-authored-by: Christopher C <37060219+cavonac@users.noreply.github.com>
Co-authored-by: Monica Gupta <scorpio90m@gmail.com>
Co-authored-by: Monica Gupta <mogupt@microsoft.com>
Co-authored-by: Benjin Dubishar <benjin.dubishar@gmail.com>
Co-authored-by: Karl Burtram <karlb@microsoft.com>
Co-authored-by: Sakshi Sharma <57200045+SakshiS-harma@users.noreply.github.com>
Co-authored-by: Vasu Bhog <vabhog@microsoft.com>
Co-authored-by: Alan Ren <alanren@microsoft.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lucy Zhang <luczhan@microsoft.com>
Co-authored-by: Leila Lali <llali@microsoft.com>
Co-authored-by: Kim Santiago <31145923+kisantia@users.noreply.github.com>
Co-authored-by: Aasim Khan <aasimkhan30@gmail.com>
Co-authored-by: Sai Avishkar Sreerama <74571829+ssreerama@users.noreply.github.com>
Co-authored-by: Aditya Bist <adbist@microsoft.com>
Co-authored-by: Arvind Ranasaria <ranasaria@outlook.com>
Co-authored-by: Benjin Dubishar <benjin@Largo.local>
Co-authored-by: Sai Avishkar Sreerama <ssreerama@microsoft.com>
Co-authored-by: Drew Skwiers-Koballa <dzsquared@users.noreply.github.com>
Co-authored-by: Brian Bergeron <brian.e.bergeron@gmail.com>
Co-authored-by: Vladimir Chernov <v-chvlad@microsoft.com>
Co-authored-by: Alex Ma <alma1@microsoft.com>
2021-02-16 16:58:55 -08:00
Alan Ren
56a6557c6e show page first and then do the layout (#14307) 2021-02-16 14:41:39 -08:00
Kim Santiago
c84e092473 Update dacpac wizard to use folder icon (#14290)
* change dacpac extension to use folder icon instead of ... on buttons

* make folder icon more centered

* fix tests
2021-02-16 12:57:13 -08:00
Charles Gagnon
21fae18a13 Ignore loc project during hygiene (#14060)
* Ignore loc project during hygiene

* Update distro
2021-02-16 11:34:13 -08:00
Kim Santiago
0c1b16d4fb fix dacpac dropdowns selecting first db in dropdown instead of right clicked db (#14285) 2021-02-16 10:23:22 -08:00
Udeesha Gautam
f4cf506290 Changes to avoid flickering and ensuring the target DB is always selected correctly even if corresponding source is not present (#14295) 2021-02-13 20:03:53 -08:00
Alan Ren
54facabde3 remove the dirty flag (#14292) 2021-02-13 19:01:26 -08:00
Barbara Valdez
0f748a60b3 Fix not numbered inconsistency (#14269)
* fix not numbered field

* fix for adding extra field and removing it from sections

* use const for variable

* use spread operator to create new object

* address pr comments

* change function name and simplify

* Add comment and put the initializeContents in the finally block
2021-02-12 19:17:22 -08:00
Charles Gagnon
238a0c60d9 Fix error when listing MIAA databases (#14286) 2021-02-12 17:16:11 -08:00
Barbara Valdez
415689de9f Fix search results display (#14265)
* fix search results

* fix paths for windows
2021-02-12 16:51:14 -08:00
Barbara Valdez
612c385725 mark test as unstable (#14284) 2021-02-12 16:44:07 -08:00
Charles Gagnon
6a09d8387e Fix notebook connection dropdown opening multiple connection dialogs (#14283) 2021-02-12 16:42:27 -08:00
nasc17
35c8a20eb3 Added Server Group Nodes status table to Overview page (#14194)
* Add podstatus to spec

* Added image to table and fixed spacing.

* Added pod status to spec

* PR fixes

* Edited so that when page is closed and reopened, does not have empty server group node table
2021-02-12 16:24:27 -08:00
Charles Gagnon
c82e80e61f Bump arc and azdata versions (#14279) 2021-02-12 13:28:14 -08:00
Charles Gagnon
6eba54d819 Update min azdata version (#14277) 2021-02-12 13:13:04 -08:00
Charles Gagnon
56d2f6b497 Fix extension recommendations (#14275)
* Fix extension recommendations

* revert header
2021-02-12 13:00:48 -08:00
Karl Burtram
c456b81071 Update version for March insiders builds (#14272) 2021-02-12 11:57:28 -08:00
Karl Burtram
767a197780 Turn back on smoke tests (#14238) 2021-02-12 09:05:20 -08:00
Karl Burtram
692336e6a7 Turn back on Notebook unit tests (#14237)
* Turn back on Notebook unit tests

* Don't skip notebook tests

* Remove done calls from tests
2021-02-12 01:40:21 -08:00
Charles Gagnon
e8d02dbc44 Cleanup typings from vs code merge (#14267) 2021-02-11 22:18:03 -08:00
Barbara Valdez
75cda19504 Add await to refresh book method (#14236)
* add await to refresh book method

* change name of method

* Reload tree view

* address pr comments

* adding finally on finally
2021-02-11 21:57:53 -08:00
Aasim Khan
b5479d0246 Migration wizard Refresh 11th Feb 2021 (#14257)
* Adding summary page,
Storing ongoing migrations,
localizing some string,
made changes to how dropdowns work
updated database backup page

* Moved classes into different files
Fixed a lot of typos
Fixed some UI margins
2021-02-11 21:18:44 -08:00
Karl Burtram
7739f25f7f Turn back on Linux CI test validation (#14241)
* Turn on Linux CI test validation

* Add checks around a code block raising exceptions in unit tests

* Bump node version to 12

* Add check around classList add
2021-02-11 18:59:40 -08:00
Alan Ren
756454efa9 add support to accept visualization options (#14254)
* allow query provider to specify visual options

* make it reusable

* add comment

* fix error

* null check
2021-02-11 15:40:45 -08:00
Karl Burtram
c511d1a78f Bump vscode version product.json (#14262)
The previous vscode merge brought our vscode compat level up to 1.51
2021-02-11 15:31:02 -08:00
Kim Santiago
5e168b0960 fix system db references going away when project target platform is changed (#14255)
* fix system db references going away when target platform is changed

* bump version
2021-02-11 14:00:07 -08:00
Charles Gagnon
d8438bd720 Add connect button to MIAA dashboard (#14183) (#14200)
* Add connect button to MIAA dashboard

* PR comments

(cherry picked from commit da01c75dcf)
2021-02-11 10:04:17 -08:00
Charles Gagnon
49a989ac14 Remove AutomaticPortForwarding service (#14248)
* Remove AutomaticPortForwarding service

* fix hygiene
2021-02-11 07:27:32 -08:00
Chris LaFreniere
e4c5af2663 Fix notebook views (#14249) 2021-02-10 16:24:12 -08:00
Karl Burtram
6aafc252e6 Add additional electron bump metadata (#14242)
* Add additional electron bump metadata

* add yarn.lock
2021-02-10 14:48:29 -08:00
Charles Gagnon
e65a4ac13d vBump arc and azdata extensions (#14246) 2021-02-10 14:08:02 -08:00
Charles Gagnon
45adb7d8d7 Add ability to pass in initial variable values to deployment wizards (#14224) 2021-02-10 13:55:16 -08:00
Kim Santiago
bb29ae00c9 Fix schema compare options reset (#14225)
* fix schema compare options reset not updating correctly

* bump version
2021-02-10 13:46:46 -08:00
Lucy Zhang
800cd2fa89 catch getpythonuserdir error (#14193) 2021-02-10 10:13:08 -08:00
dependabot[bot]
61f9da39fa Bump stringstream from 0.0.5 to 0.0.6 in /build (#14219)
Bumps [stringstream](https://github.com/mhart/StringStream) from 0.0.5 to 0.0.6.
- [Release notes](https://github.com/mhart/StringStream/releases)
- [Commits](https://github.com/mhart/StringStream/compare/v0.0.5...v0.0.6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-10 09:21:35 -08:00
Karl Burtram
ce69ecebc4 Update readme for 1.25.3 release (#14232) 2021-02-10 09:07:58 -08:00
5298 changed files with 343054 additions and 198526 deletions

View File

@@ -11,57 +11,57 @@ on:
- release/*
jobs:
# linux:
# runs-on: ubuntu-latest
# env:
# CHILD_CONCURRENCY: "1"
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# steps:
# - uses: actions/checkout@v2.2.0
# # TODO: rename azure-pipelines/linux/xvfb.init to github-actions
# - run: |
# sudo apt-get update
# sudo apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 dbus xvfb libgtk-3-0 libkrb5-dev # {{SQL CARBON EDIT}} add kerberos dep
# sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb
# sudo chmod +x /etc/init.d/xvfb
# sudo update-rc.d xvfb defaults
# sudo service xvfb start
# name: Setup Build Environment
# - uses: actions/setup-node@v1
# with:
# node-version: 10
# # TODO: cache node modules
# # Increase timeout to get around latency issues when fetching certain packages
# - run: |
# yarn config set network-timeout 300000
# yarn --frozen-lockfile
# name: Install Dependencies
# - run: yarn electron x64
# name: Download Electron
# - run: yarn gulp hygiene
# name: Run Hygiene Checks
# - run: yarn strict-vscode # {{SQL CARBON EDIT}} add step
# name: Run Strict Compile Options
# # - run: yarn monaco-compile-check {{SQL CARBON EDIT}} remove step
# # name: Run Monaco Editor Checks
# - run: yarn valid-layers-check
# name: Run Valid Layers Checks
# - run: yarn compile
# name: Compile Sources
# # - run: yarn download-builtin-extensions {{SQL CARBON EDIT}} remove step
# # name: Download Built-in Extensions
# - run: DISPLAY=:10 ./scripts/test.sh --tfs "Unit Tests" --coverage --runGlob "**/sql/**/*.test.js"
# name: Run Unit Tests (Electron)
# - run: DISPLAY=:10 ./scripts/test-extensions-unit.sh
# name: Run Extension Unit Tests (Electron)
# # {{SQL CARBON EDIT}} Add coveralls. We merge first to get around issue where parallel builds weren't being combined correctly
# - run: node test/combineCoverage
# name: Combine code coverage files
# - name: Upload Code Coverage
# uses: coverallsapp/github-action@v1.1.1
# with:
# github-token: ${{ secrets.GITHUB_TOKEN }}
# path-to-lcov: "test/coverage/lcov.info"
linux:
runs-on: ubuntu-latest
env:
CHILD_CONCURRENCY: "1"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v2.2.0
# TODO: rename azure-pipelines/linux/xvfb.init to github-actions
- run: |
sudo apt-get update
sudo apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 dbus xvfb libgtk-3-0 libkrb5-dev # {{SQL CARBON EDIT}} add kerberos dep
sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb
sudo chmod +x /etc/init.d/xvfb
sudo update-rc.d xvfb defaults
sudo service xvfb start
name: Setup Build Environment
- uses: actions/setup-node@v1
with:
node-version: 12
# TODO: cache node modules
# Increase timeout to get around latency issues when fetching certain packages
- run: |
yarn config set network-timeout 300000
yarn --frozen-lockfile
name: Install Dependencies
- run: yarn electron x64
name: Download Electron
- run: yarn gulp hygiene
name: Run Hygiene Checks
- run: yarn strict-vscode # {{SQL CARBON EDIT}} add step
name: Run Strict Compile Options
# - run: yarn monaco-compile-check {{SQL CARBON EDIT}} remove step
# name: Run Monaco Editor Checks
- run: yarn valid-layers-check
name: Run Valid Layers Checks
- run: yarn compile
name: Compile Sources
# - run: yarn download-builtin-extensions {{SQL CARBON EDIT}} remove step
# name: Download Built-in Extensions
- run: DISPLAY=:10 ./scripts/test.sh --tfs "Unit Tests" --coverage --runGlob "**/sql/**/*.test.js"
name: Run Unit Tests (Electron)
- run: DISPLAY=:10 ./scripts/test-extensions-unit.sh
name: Run Extension Unit Tests (Electron)
# {{SQL CARBON EDIT}} Add coveralls. We merge first to get around issue where parallel builds weren't being combined correctly
- run: node test/combineCoverage
name: Combine code coverage files
- name: Upload Code Coverage
uses: coverallsapp/github-action@v1.1.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: "test/coverage/lcov.info"
# Fails with cryptic error (e.g. https://github.com/microsoft/vscode/pull/90292/checks?check_run_id=433681926#step:13:9)
# - run: DISPLAY=:10 yarn test-browser --browser chromium
@@ -78,7 +78,7 @@ jobs:
- uses: actions/checkout@v2.2.0
- uses: actions/setup-node@v1
with:
node-version: 10
node-version: 12
- uses: actions/setup-python@v1
with:
python-version: "2.x"
@@ -117,7 +117,7 @@ jobs:
- uses: actions/checkout@v2.2.0
- uses: actions/setup-node@v1
with:
node-version: 10
node-version: 12
# Increase timeout to get around latency issues when fetching certain packages
- run: |
yarn config set network-timeout 300000

View File

@@ -1,5 +1,50 @@
# Change Log
## Version 1.27.0
* Release date: March 17, 2021
* Release status: General Availability
* New Notebook Features:
* Added create book dialog
* Extension Updates:
* Import
* Dacpac
* Machine Learning
* SQL Assessment
* Arc
* SQL Database Projects
* ASDE Deployment
* Bux Fixes
## Version 1.26.1
* Release date: February 25, 2021
* Release status: General Availability
* Fixes https://github.com/microsoft/azuredatastudio/issues/14382
## Version 1.26.0
* Release date: February 22, 2021
* Release status: General Availability
* Added edit Jupyter book UI support
* Improved Jupyter server start-up time by 50% on windows
* Extension Updates:
* Azure Arc
* PG dashboard enhancements
* Multi-controller support
* MIAA Dashboard will no longer prompt for SQL Server connection immediately upon opening
* Azure Data CLI
* Kusto
* Machine Learning
* Profiler
* Server Reports
* Schema Compare
* SQL Server Dacpac
* SQL Database Projects
* Bug Fixes
## Version 1.25.3
* Release date: February 10, 2021
* Release status: General Availability
* Update Electron to 9.4.3 to incorporate critical upstream fixes
## Version 1.25.2
* Release date: January 22, 2021
* Release status: General Availability

View File

@@ -131,10 +131,10 @@ Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the [Source EULA](LICENSE.txt).
[win-user]: https://go.microsoft.com/fwlink/?linkid=2150927
[win-system]: https://go.microsoft.com/fwlink/?linkid=2150928
[win-zip]: https://go.microsoft.com/fwlink/?linkid=2151312
[osx-zip]: https://go.microsoft.com/fwlink/?linkid=2151311
[linux-zip]: https://go.microsoft.com/fwlink/?linkid=2151508
[linux-rpm]: https://go.microsoft.com/fwlink/?linkid=2151407
[linux-deb]: https://go.microsoft.com/fwlink/?linkid=2151506
[win-user]: https://go.microsoft.com/fwlink/?linkid=2157460
[win-system]: https://go.microsoft.com/fwlink/?linkid=2157459
[win-zip]: https://go.microsoft.com/fwlink/?linkid=2157458
[osx-zip]: https://go.microsoft.com/fwlink/?linkid=2157456
[linux-zip]: https://go.microsoft.com/fwlink/?linkid=2157353
[linux-rpm]: https://go.microsoft.com/fwlink/?linkid=2157248
[linux-deb]: https://go.microsoft.com/fwlink/?linkid=2157352

View File

@@ -12,7 +12,7 @@ expressly granted herein, whether by implication, estoppel or otherwise.
angular2-grid: https://github.com/BTMorton/angular2-grid
angular2-slickgrid: https://github.com/Microsoft/angular2-slickgrid
applicationinsights: https://github.com/Microsoft/ApplicationInsights-node.js
axios: https://github.com/axios/axios
axios: https://github.com/axios/axios
bootstrap: https://github.com/twbs/bootstrap
chart.js: https://github.com/Timer/chartjs
chokidar: https://github.com/paulmillr/chokidar
@@ -39,7 +39,7 @@ expressly granted herein, whether by implication, estoppel or otherwise.
jschardet: https://github.com/aadsm/jschardet
jupyter-powershell: https://github.com/vors/jupyter-powershell
JupyterLab: https://github.com/jupyterlab/jupyterlab
keytar: https://github.com/atom/node-keytar
keytar: https://github.com/atom/node-keytar
make-error: https://github.com/JsCommunity/make-error
mark.js: https://github.com/julmot/mark.js
minimist: https://github.com/substack/minimist
@@ -54,7 +54,8 @@ expressly granted herein, whether by implication, estoppel or otherwise.
primeng: https://github.com/primefaces/primeng
process-nextick-args: https://github.com/calvinmetcalf/process-nextick-args
pty.js: https://github.com/chjj/pty.js
qs: https://github.com/ljharb/qs
pyzmq: https://github.com/zeromq/pyzmq
qs: https://github.com/ljharb/qs
reflect-metadata: https://github.com/rbuckton/reflect-metadata
request: https://github.com/request/request
rxjs: https://github.com/ReactiveX/RxJS
@@ -1548,6 +1549,40 @@ THE SOFTWARE.
=========================================
END OF pty.js NOTICES AND INFORMATION
%% PyZMQ NOTICES AND INFORMATION BEGIN HERE
=========================================
Copyright (c) 2009-2012, Brian Granger, Min Ragan-Kelley
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
Neither the name of PyZMQ nor the names of its contributors may be used to
endorse or promote products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
=========================================
END OF pyzmq NOTICES AND INFORMATION
%% reflect-metadata NOTICES AND INFORMATION BEGIN HERE
=========================================
Apache License

View File

@@ -122,14 +122,14 @@ steps:
displayName: Run integration tests (Electron)
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'))
# - script: |
# set -e
# APP_ROOT=$(agent.builddirectory)/azuredatastudio-darwin-x64
# APP_NAME="`ls $APP_ROOT | head -n 1`"
# yarn smoketest --build "$APP_ROOT/$APP_NAME" --screenshots "$(build.artifactstagingdirectory)/smokeshots" --log "$(build.artifactstagingdirectory)/logs/darwin/smoke.log"
# displayName: Run smoke tests (Electron)
# continueOnError: true
# condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'))
- script: |
set -e
APP_ROOT=$(agent.builddirectory)/azuredatastudio-darwin-x64
APP_NAME="`ls $APP_ROOT | head -n 1`"
yarn smoketest --build "$APP_ROOT/$APP_NAME" --screenshots "$(build.artifactstagingdirectory)/smokeshots" --log "$(build.artifactstagingdirectory)/logs/darwin/smoke.log"
displayName: Run smoke tests (Electron)
continueOnError: true
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'))
# - script: |
# set -e

View File

@@ -46,8 +46,7 @@ jobs:
steps:
- template: linux/sql-product-build-linux.yml
parameters:
# extensionsToUnitTest: ["admin-tool-ext-win", "agent", "azdata", "azurecore", "cms", "dacpac", "import", "schema-compare", "notebook", "resource-deployment", "machine-learning", "sql-database-projects", "data-workspace"]
extensionsToUnitTest: ["admin-tool-ext-win", "agent", "azdata", "azurecore", "cms", "dacpac", "import", "schema-compare", "resource-deployment", "machine-learning", "sql-database-projects", "data-workspace"]
extensionsToUnitTest: ["admin-tool-ext-win", "agent", "azdata", "azurecore", "cms", "dacpac", "import", "schema-compare", "notebook", "resource-deployment", "machine-learning", "sql-database-projects", "data-workspace"]
timeoutInMinutes: 70
- job: LinuxWeb

View File

@@ -151,7 +151,7 @@ steps:
inputs:
ConnectedServiceName: 'Code Signing'
FolderPath: '$(agent.builddirectory)/azuredatastudio-win32-x64'
Pattern: '*.exe,*.node,resources/app/node_modules.asar.unpacked/*.dll,swiftshader/*.dll,d3dcompiler_47.dll,libGLESv2.dll,ffmpeg.dll,libEGL.dll,Microsoft.SqlTools.Hosting.dll,Microsoft.SqlTools.ResourceProvider.Core.dll,Microsoft.SqlTools.ResourceProvider.DefaultImpl.dll,MicrosoftSqlToolsCredentials.dll,MicrosoftSqlToolsServiceLayer.dll,Newtonsoft.Json.dll,SqlSerializationService.dll,SqlToolsResourceProviderService.dll,Microsoft.SqlServer.*.dll,Microsoft.Data.Tools.Sql.BatchParser.dll'
Pattern: '*.exe,*.node,resources/app/node_modules.asar.unpacked/*.dll,swiftshader/*.dll,d3dcompiler_47.dll,vulkan-1.dll,libGLESv2.dll,ffmpeg.dll,libEGL.dll,Microsoft.SqlTools.Hosting.dll,Microsoft.SqlTools.ResourceProvider.Core.dll,Microsoft.SqlTools.ResourceProvider.DefaultImpl.dll,MicrosoftSqlToolsCredentials.dll,MicrosoftSqlToolsServiceLayer.dll,Newtonsoft.Json.dll,SqlSerializationService.dll,SqlToolsResourceProviderService.dll,Microsoft.SqlServer.*.dll,Microsoft.Data.Tools.Sql.BatchParser.dll'
signConfigType: inlineSignParams
inlineOperation: |
[

View File

@@ -30,7 +30,7 @@ const all = [
'test/**/*',
'!test/**/out/**',
'!**/node_modules/**',
'!build/actions/**/*.js', // {{ SQL CARBON EDIT }}
'!build/actions/**/*.js', // {{SQL CARBON EDIT}}
'!build/**/*' // {{SQL CARBON EDIT}}
];
module.exports.all = all;
@@ -114,6 +114,9 @@ const indentationFilter = [
'!extensions/big-data-cluster/src/bigDataCluster/controller/apiGenerated.ts',
'!extensions/big-data-cluster/src/bigDataCluster/controller/clusterApiGenerated2.ts',
'!resources/linux/snap/electron-launch',
'!extensions/markdown-language-features/media/*.js',
'!extensions/simple-browser/media/*.js',
'!resources/xlf/LocProject.json', // {{SQL CARBON EDIT}}
'!build/**/*' // {{SQL CARBON EDIT}}
];

View File

@@ -3235,9 +3235,9 @@ string_decoder@~0.10.x:
integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
stringstream@~0.0.4, stringstream@~0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
integrity sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=
version "0.0.6"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72"
integrity sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==
strip-ansi@^3.0.0:
version "3.0.1"
@@ -3751,9 +3751,9 @@ xtend@~4.0.1:
integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
y18n@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
version "4.0.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==
yallist@^4.0.0:
version "4.0.0"

View File

@@ -60,12 +60,12 @@
"git": {
"name": "electron",
"repositoryUrl": "https://github.com/electron/electron",
"commitHash": "415c1f9e9b35d9599b1a8ad1200476afa47a3323"
"commitHash": "ca82414364002efa665ffa7427e267adf76ed1f3"
}
},
"isOnlyProductionDependency": true,
"license": "MIT",
"version": "9.3.5"
"version": "9.4.3"
},
{
"component": {

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 18 18" style="enable-background:new 0 0 18 18;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
</style>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="9" y1="2" x2="9" y2="20" gradientTransform="matrix(1 0 0 -1 0 20)">
<stop offset="0" style="stop-color:#32BEDD"/>
<stop offset="0.576" style="stop-color:#32CEEF"/>
<stop offset="1" style="stop-color:#32D4F5"/>
</linearGradient>
<path class="st0" d="M18,10V7.9l-0.3-0.1l-2.2-0.7l-0.6-1.4l1.1-2.4l-1.4-1.4L14.3,2l-2,1l-1.4-0.6L10,0H7.9L7.8,0.3L7.1,2.5
L5.7,3.1L3.3,1.9L1.9,3.4l0.1,0.3l1,2.1L2.5,7.1L0,8v2.1l0.3,0.1l2.2,0.7l0.6,1.4l-1.1,2.4l1.4,1.4L3.7,16l2.1-1l1.4,0.6L8,18h2.1
l0.1-0.3l0.7-2.2l1.4-0.6l2.4,1.1l1.4-1.4L16,14.3l-1-2l0.6-1.4L18,10z M9,12.9c-2.2,0-4-1.8-4-3.9s1.8-4,4-4s3.9,1.8,3.9,4l0,0
C13,11.2,11.2,12.9,9,12.9C9,13,9,13,9,12.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 18 18" style="enable-background:new 0 0 18 18;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
</style>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="9" y1="2" x2="9" y2="20" gradientTransform="matrix(1 0 0 -1 0 20)">
<stop offset="1.000000e-03" style="stop-color:#767676"/>
<stop offset="1" style="stop-color:#D9D9D9"/>
</linearGradient>
<path class="st0" d="M18,10V7.9l-0.3-0.1l-2.2-0.7l-0.6-1.4l1.1-2.4l-1.4-1.4L14.3,2l-2,1l-1.4-0.6L10,0H7.9L7.8,0.3L7.1,2.5
L5.7,3.1L3.3,1.9L1.9,3.4l0.1,0.3l1,2.1L2.5,7.1L0,8v2.1l0.3,0.1l2.2,0.7l0.6,1.4l-1.1,2.4l1.4,1.4L3.7,16l2.1-1l1.4,0.6L8,18h2.1
l0.1-0.3l0.7-2.2l1.4-0.6l2.4,1.1l1.4-1.4L16,14.3l-1-2l0.6-1.4L18,10z M9,12.9c-2.2,0-4-1.8-4-3.9s1.8-4,4-4s3.9,1.8,3.9,4l0,0
C13,11.2,11.2,12.9,9,12.9C9,13,9,13,9,12.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M13.95,7.75h0v.3h0l2,1.3-1.2,3-2.4-.3-.3.3.5,2.4-3,1.2-1.3-2h-.5l-1.3,2-3-1.2.6-2.4-.4-.3-2.4.5-1.2-3,2-1.3h0v-.4h0l-2-1.4,1.2-3,2.4.6a.8.8,0,0,1,.4-.4l-.6-2.4,3-1.2,1.3,2h.5l1.3-2,3,1.2-.5,2.4.3.4,2.4-.6,1.2,3Zm-.9,1V7.25l1.8-1.1-.7-1.7-2.1.5a1.205,1.205,0,0,0-.5-.5,1.205,1.205,0,0,0-.5-.5l.5-2.1-1.7-.7-1.1,1.8H7.25l-1.1-1.8-1.7.7.5,2.1a1.205,1.205,0,0,0-.5.5,1.205,1.205,0,0,0-.5.5l-2.1-.5-.7,1.7,1.8,1.1v1.5l-1.8,1.1.7,1.7,2.1-.5.5.5.5.5-.5,2.1,1.7.7,1.1-1.8h1.5l1.1,1.8,1.7-.7-.5-2.1.5-.5.5-.5,2.1.5.7-1.7Zm-5-3.7,1.1.2a2.2,2.2,0,0,1,.9.7,3.1,3.1,0,0,1,.7.9,4.3,4.3,0,0,1,.2,1.2,4.328,4.328,0,0,1-.2,1.1,2.2,2.2,0,0,1-.7.9,2.2,2.2,0,0,1-.9.7l-1.1.2-1.2-.2a3.1,3.1,0,0,1-.9-.7,2.2,2.2,0,0,1-.7-.9,4.328,4.328,0,0,1-.2-1.1,4.3,4.3,0,0,1,.2-1.2,3.1,3.1,0,0,1,.7-.9,3.1,3.1,0,0,1,.9-.7Zm0,5h.8l.6-.5.5-.6a2.489,2.489,0,0,0,.1-.9,2.793,2.793,0,0,0-.1-.9l-.5-.6-.6-.4a1.3,1.3,0,0,0-.8-.2,1.507,1.507,0,0,0-.9.2l-.6.4-.4.6a1.507,1.507,0,0,0-.2.9,1.3,1.3,0,0,0,.2.8l.4.6.6.5Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M13.95,7.75h0v.3h0l2,1.3-1.2,3-2.4-.3-.3.3.5,2.4-3,1.2-1.3-2h-.5l-1.3,2-3-1.2.6-2.4-.4-.3-2.4.5-1.2-3,2-1.3h0v-.4h0l-2-1.4,1.2-3,2.4.6a.8.8,0,0,1,.4-.4l-.6-2.4,3-1.2,1.3,2h.5l1.3-2,3,1.2-.5,2.4.3.4,2.4-.6,1.2,3Zm-.9,1V7.25l1.8-1.1-.7-1.7-2.1.5a1.205,1.205,0,0,0-.5-.5,1.205,1.205,0,0,0-.5-.5l.5-2.1-1.7-.7-1.1,1.8H7.25l-1.1-1.8-1.7.7.5,2.1a1.205,1.205,0,0,0-.5.5,1.205,1.205,0,0,0-.5.5l-2.1-.5-.7,1.7,1.8,1.1v1.5l-1.8,1.1.7,1.7,2.1-.5.5.5.5.5-.5,2.1,1.7.7,1.1-1.8h1.5l1.1,1.8,1.7-.7-.5-2.1.5-.5.5-.5,2.1.5.7-1.7Zm-5-3.7,1.1.2a2.2,2.2,0,0,1,.9.7,3.1,3.1,0,0,1,.7.9,4.3,4.3,0,0,1,.2,1.2,4.328,4.328,0,0,1-.2,1.1,2.2,2.2,0,0,1-.7.9,2.2,2.2,0,0,1-.9.7l-1.1.2-1.2-.2a3.1,3.1,0,0,1-.9-.7,2.2,2.2,0,0,1-.7-.9,4.328,4.328,0,0,1-.2-1.1,4.3,4.3,0,0,1,.2-1.2,3.1,3.1,0,0,1,.7-.9,3.1,3.1,0,0,1,.9-.7Zm0,5h.8l.6-.5.5-.6a2.489,2.489,0,0,0,.1-.9,2.793,2.793,0,0,0-.1-.9l-.5-.6-.6-.4a1.3,1.3,0,0,0-.8-.2,1.507,1.507,0,0,0-.9.2l-.6.4-.4.6a1.507,1.507,0,0,0-.2.9,1.3,1.3,0,0,0,.2.8l.4.6.6.5Z" fill="#0078D4" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

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="M13.9297 7.71875C13.9297 7.76562 13.9297 7.8125 13.9297 7.85938C13.9349 7.90625 13.9375 7.95312 13.9375 8C13.9375 8.04688 13.9349 8.09375 13.9297 8.14062C13.9297 8.1875 13.9297 8.23438 13.9297 8.28125L15.9531 9.53906L14.7109 12.5312L12.3906 12C12.2656 12.1354 12.1354 12.2656 12 12.3906L12.5312 14.7109L9.53906 15.9531L8.28125 13.9297C8.23438 13.9297 8.1875 13.9323 8.14062 13.9375C8.09375 13.9375 8.04688 13.9375 8 13.9375C7.95312 13.9375 7.90625 13.9375 7.85938 13.9375C7.8125 13.9323 7.76562 13.9297 7.71875 13.9297L6.46094 15.9531L3.46875 14.7109L4 12.3906C3.86458 12.2656 3.73438 12.1354 3.60938 12L1.28906 12.5312L0.046875 9.53906L2.07031 8.28125C2.07031 8.23438 2.06771 8.1875 2.0625 8.14062C2.0625 8.09375 2.0625 8.04688 2.0625 8C2.0625 7.95312 2.0625 7.90625 2.0625 7.85938C2.06771 7.8125 2.07031 7.76562 2.07031 7.71875L0.046875 6.46094L1.28906 3.46875L3.60938 4C3.73438 3.86458 3.86458 3.73438 4 3.60938L3.46875 1.28906L6.46094 0.046875L7.71875 2.07031C7.76562 2.07031 7.8125 2.07031 7.85938 2.07031C7.90625 2.0651 7.95312 2.0625 8 2.0625C8.04688 2.0625 8.09375 2.0651 8.14062 2.07031C8.1875 2.07031 8.23438 2.07031 8.28125 2.07031L9.53906 0.046875L12.5312 1.28906L12 3.60938C12.1354 3.73438 12.2656 3.86458 12.3906 4L14.7109 3.46875L15.9531 6.46094L13.9297 7.71875ZM13.0156 8.73438C13.026 8.60938 13.0365 8.48698 13.0469 8.36719C13.0573 8.24219 13.0625 8.11719 13.0625 7.99219C13.0625 7.8724 13.0573 7.75 13.0469 7.625C13.0365 7.5 13.026 7.3776 13.0156 7.25781L14.8594 6.10938L14.1875 4.48438L12.0703 4.97656C11.9089 4.77865 11.7422 4.59635 11.5703 4.42969C11.4036 4.26302 11.2214 4.09635 11.0234 3.92969L11.5156 1.8125L9.89062 1.14062L8.73438 2.98438C8.61458 2.97396 8.49219 2.96354 8.36719 2.95312C8.24219 2.94271 8.11979 2.9375 8 2.9375C7.875 2.9375 7.75 2.94271 7.625 2.95312C7.50521 2.96354 7.38281 2.97396 7.25781 2.98438L6.10938 1.14062L4.48438 1.8125L4.97656 3.92969C4.77865 4.09115 4.59635 4.25781 4.42969 4.42969C4.26302 4.59635 4.09635 4.77865 3.92969 4.97656L1.8125 4.48438L1.14062 6.10938L2.98438 7.26562C2.97396 7.39062 2.96354 7.51562 2.95312 7.64062C2.94271 7.76042 2.9375 7.88281 2.9375 8.00781C2.9375 8.1276 2.94271 8.25 2.95312 8.375C2.96354 8.5 2.97396 8.6224 2.98438 8.74219L1.14062 9.89062L1.8125 11.5156L3.92969 11.0234C4.09115 11.2214 4.25521 11.4036 4.42188 11.5703C4.59375 11.737 4.77865 11.9036 4.97656 12.0703L4.48438 14.1875L6.10938 14.8594L7.26562 13.0156C7.38542 13.026 7.50781 13.0365 7.63281 13.0469C7.75781 13.0573 7.88021 13.0625 8 13.0625C8.125 13.0625 8.2474 13.0573 8.36719 13.0469C8.49219 13.0365 8.61719 13.026 8.74219 13.0156L9.89062 14.8594L11.5156 14.1875L11.0234 12.0703C11.2214 11.9089 11.4036 11.7448 11.5703 11.5781C11.737 11.4062 11.9036 11.2214 12.0703 11.0234L14.1875 11.5156L14.8594 9.89062L13.0156 8.73438ZM8 5.0625C8.40625 5.0625 8.78646 5.14062 9.14062 5.29688C9.5 5.44792 9.8125 5.65625 10.0781 5.92188C10.3438 6.1875 10.5521 6.5 10.7031 6.85938C10.8594 7.21354 10.9375 7.59375 10.9375 8C10.9375 8.40625 10.8594 8.78906 10.7031 9.14844C10.5521 9.5026 10.3438 9.8125 10.0781 10.0781C9.8125 10.3438 9.5 10.5547 9.14062 10.7109C8.78646 10.862 8.40625 10.9375 8 10.9375C7.59375 10.9375 7.21094 10.862 6.85156 10.7109C6.4974 10.5547 6.1875 10.3438 5.92188 10.0781C5.65625 9.8125 5.44531 9.5026 5.28906 9.14844C5.13802 8.78906 5.0625 8.40625 5.0625 8C5.0625 7.59375 5.13802 7.21354 5.28906 6.85938C5.44531 6.5 5.65625 6.1875 5.92188 5.92188C6.1875 5.65625 6.4974 5.44792 6.85156 5.29688C7.21094 5.14062 7.59375 5.0625 8 5.0625ZM8 10.0625C8.28646 10.0625 8.55469 10.0104 8.80469 9.90625C9.05469 9.79688 9.27344 9.64844 9.46094 9.46094C9.64844 9.27344 9.79427 9.05469 9.89844 8.80469C10.0078 8.55469 10.0625 8.28646 10.0625 8C10.0625 7.71354 10.0078 7.44531 9.89844 7.19531C9.79427 6.94531 9.64844 6.72656 9.46094 6.53906C9.27344 6.35156 9.05469 6.20573 8.80469 6.10156C8.55469 5.99219 8.28646 5.9375 8 5.9375C7.71354 5.9375 7.44531 5.99219 7.19531 6.10156C6.94531 6.20573 6.72656 6.35156 6.53906 6.53906C6.35156 6.72656 6.20312 6.94531 6.09375 7.19531C5.98958 7.44531 5.9375 7.71354 5.9375 8C5.9375 8.28646 5.98958 8.55469 6.09375 8.80469C6.20312 9.05469 6.35156 9.27344 6.53906 9.46094C6.72656 9.64844 6.94531 9.79688 7.19531 9.90625C7.44531 10.0104 7.71354 10.0625 8 10.0625Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -1,10 +0,0 @@
<svg id="a2f0dd32-c564-48d6-97d7-86323bfba35b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18">
<defs>
<linearGradient id="b863127b-2eb8-42a1-a46b-989a6a8d258c" x1="9" y1="18" x2="9" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#32bedd" />
<stop offset="0.576" stop-color="#32ceef" />
<stop offset="1" stop-color="#32d4f5" />
</linearGradient>
</defs>
<path d="M18,9.972V7.92l-.288-.108-2.2-.72-.576-1.4,1.116-2.376-1.44-1.44-.288.144L12.276,3.06l-1.4-.576L9.972,0H7.92L7.812.288l-.72,2.2-1.4.576L3.348,1.944l-1.44,1.44.144.288L3.1,5.724l-.576,1.4L0,8.028V10.08l.288.108,2.2.72.576,1.4L1.944,14.688l1.44,1.44.288-.144L5.724,14.94l1.4.576L8.028,18H10.08l.108-.288.72-2.2,1.4-.576,2.376,1.116,1.44-1.44-.144-.288L14.94,12.276l.576-1.4ZM9,12.95A3.95,3.95,0,1,1,12.95,9,3.947,3.947,0,0,1,9,12.95Z" fill="url(#b863127b-2eb8-42a1-a46b-989a6a8d258c)" />
</svg>

Before

Width:  |  Height:  |  Size: 909 B

View File

@@ -1,2 +1 @@
title: Azure Arc Data Services
description: A collection of notebooks to support Azure Arc Data Services.
title: Azure Arc Data Services

View File

@@ -1,12 +1,10 @@
- title: Welcome
url: /readme
not_numbered: true
- title: Search
search: true
- title: Postgres
url: /postgres/readme
not_numbered: true
expand_sections: true
sections:
- title: TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter
url: postgres/tsg100-troubleshoot-postgres
- title: Postgres
url: /postgres/readme
not_numbered: true
sections:
- title: TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter
url: postgres/tsg100-troubleshoot-postgres

View File

@@ -2,6 +2,10 @@
- This chapter contains notebooks for troubleshooting Postgres on Azure Arc
## Notebooks in this Chapter
- [TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter](tsg100-troubleshoot-postgres.ipynb)
[Home](../readme.md)
## Notebooks in this Chapter
- [TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter](../postgres/tsg100-troubleshoot-postgres.ipynb)

View File

@@ -1,7 +0,0 @@
- title: Postgres
url: /postgres/readme
not_numbered: true
expand_sections: true
sections:
- title: TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter
url: postgres/tsg100-troubleshoot-postgres

View File

@@ -2,7 +2,11 @@
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter\n",
"===================================================================\n",
@@ -35,14 +39,17 @@
"# the user will be prompted to select a server.\n",
"namespace = os.environ.get('POSTGRES_SERVER_NAMESPACE')\n",
"name = os.environ.get('POSTGRES_SERVER_NAME')\n",
"version = os.environ.get('POSTGRES_SERVER_VERSION')\n",
"\n",
"tail_lines = 50"
]
},
{
"cell_type": "markdown",
"metadata": {},
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"### Common functions\n",
"\n",
@@ -63,7 +70,6 @@
"import sys\n",
"import os\n",
"import re\n",
"import json\n",
"import platform\n",
"import shlex\n",
"import shutil\n",
@@ -76,11 +82,7 @@
"error_hints = {} # Output in stderr where a known SOP/TSG exists which will be HINTed for further help\n",
"install_hint = {} # The SOP to help install the executable if it cannot be found\n",
"\n",
"first_run = True\n",
"rules = None\n",
"debug_logging = False\n",
"\n",
"def run(cmd, return_output=False, no_output=False, retry_count=0):\n",
"def run(cmd, return_output=False, no_output=False, retry_count=0, base64_decode=False, return_as_json=False):\n",
" \"\"\"Run shell command, stream stdout, print stderr and optionally return output\n",
"\n",
" NOTES:\n",
@@ -103,13 +105,6 @@
" output = \"\"\n",
" retry = False\n",
"\n",
" global first_run\n",
" global rules\n",
"\n",
" if first_run:\n",
" first_run = False\n",
" rules = load_rules()\n",
"\n",
" # When running `azdata sql query` on Windows, replace any \\n in \"\"\" strings, with \" \", otherwise we see:\n",
" #\n",
" # ('HY090', '[HY090] [Microsoft][ODBC Driver Manager] Invalid string or buffer length (0) (SQLExecDirectW)')\n",
@@ -172,7 +167,12 @@
" if which_binary == None:\n",
" which_binary = shutil.which(cmd_actual[0])\n",
"\n",
" # Display an install HINT, so the user can click on a SOP to install the missing binary\n",
" #\n",
" if which_binary == None:\n",
" print(f\"The path used to search for '{cmd_actual[0]}' was:\")\n",
" print(sys.path)\n",
"\n",
" if user_provided_exe_name in install_hint and install_hint[user_provided_exe_name] is not None:\n",
" display(Markdown(f'HINT: Use [{install_hint[user_provided_exe_name][0]}]({install_hint[user_provided_exe_name][1]}) to resolve this issue.'))\n",
"\n",
@@ -219,8 +219,6 @@
" break # otherwise infinite hang, have not worked out why yet.\n",
" else:\n",
" print(line, end='')\n",
" if rules is not None:\n",
" apply_expert_rules(line)\n",
"\n",
" if wait:\n",
" p.wait()\n",
@@ -276,25 +274,22 @@
" if line_decoded.find(error_hint[0]) != -1:\n",
" display(Markdown(f'HINT: Use [{error_hint[1]}]({error_hint[2]}) to resolve this issue.'))\n",
"\n",
" # apply expert rules (to run follow-on notebooks), based on output\n",
" #\n",
" if rules is not None:\n",
" apply_expert_rules(line_decoded)\n",
"\n",
" # Verify if a transient error, if so automatically retry (recursive)\n",
" #\n",
" if user_provided_exe_name in retry_hints:\n",
" for retry_hint in retry_hints[user_provided_exe_name]:\n",
" if line_decoded.find(retry_hint) != -1:\n",
" if retry_count < MAX_RETRIES:\n",
" if retry_count \u003c MAX_RETRIES:\n",
" print(f\"RETRY: {retry_count} (due to: {retry_hint})\")\n",
" retry_count = retry_count + 1\n",
" output = run(cmd, return_output=return_output, retry_count=retry_count)\n",
"\n",
" if return_output:\n",
" return output\n",
" else:\n",
" return\n",
" if base64_decode:\n",
" import base64\n",
" return base64.b64decode(output).decode('utf-8')\n",
" else:\n",
" return output\n",
"\n",
" elapsed = datetime.datetime.now().replace(microsecond=0) - start_time\n",
"\n",
@@ -311,78 +306,31 @@
" print(f'\\nSUCCESS: {elapsed}s elapsed.\\n')\n",
"\n",
" if return_output:\n",
" return output\n",
"\n",
"def load_json(filename):\n",
" \"\"\"Load a json file from disk and return the contents\"\"\"\n",
"\n",
" with open(filename, encoding=\"utf8\") as json_file:\n",
" return json.load(json_file)\n",
"\n",
"def load_rules():\n",
" \"\"\"Load any 'expert rules' from the metadata of this notebook (.ipynb) that should be applied to the stderr of the running executable\"\"\"\n",
"\n",
" # Load this notebook as json to get access to the expert rules in the notebook metadata.\n",
" #\n",
" try:\n",
" j = load_json(\"tsg100-troubleshoot-postgres.ipynb\")\n",
" except:\n",
" pass # If the user has renamed the book, we can't load ourself. NOTE: Is there a way in Jupyter, to know your own filename?\n",
" else:\n",
" if \"metadata\" in j and \\\n",
" \"azdata\" in j[\"metadata\"] and \\\n",
" \"expert\" in j[\"metadata\"][\"azdata\"] and \\\n",
" \"expanded_rules\" in j[\"metadata\"][\"azdata\"][\"expert\"]:\n",
"\n",
" rules = j[\"metadata\"][\"azdata\"][\"expert\"][\"expanded_rules\"]\n",
"\n",
" rules.sort() # Sort rules, so they run in priority order (the [0] element). Lowest value first.\n",
"\n",
" # print (f\"EXPERT: There are {len(rules)} rules to evaluate.\")\n",
"\n",
" return rules\n",
"\n",
"def apply_expert_rules(line):\n",
" \"\"\"Determine if the stderr line passed in, matches the regular expressions for any of the 'expert rules', if so\n",
" inject a 'HINT' to the follow-on SOP/TSG to run\"\"\"\n",
"\n",
" global rules\n",
"\n",
" for rule in rules:\n",
" notebook = rule[1]\n",
" cell_type = rule[2]\n",
" output_type = rule[3] # i.e. stream or error\n",
" output_type_name = rule[4] # i.e. ename or name \n",
" output_type_value = rule[5] # i.e. SystemExit or stdout\n",
" details_name = rule[6] # i.e. evalue or text \n",
" expression = rule[7].replace(\"\\\\*\", \"*\") # Something escaped *, and put a \\ in front of it!\n",
"\n",
" if debug_logging:\n",
" print(f\"EXPERT: If rule '{expression}' satisfied', run '{notebook}'.\")\n",
"\n",
" if re.match(expression, line, re.DOTALL):\n",
"\n",
" if debug_logging:\n",
" print(\"EXPERT: MATCH: name = value: '{0}' = '{1}' matched expression '{2}', therefore HINT '{4}'\".format(output_type_name, output_type_value, expression, notebook))\n",
"\n",
" match_found = True\n",
"\n",
" display(Markdown(f'HINT: Use [{notebook}]({notebook}) to resolve this issue.'))\n",
" if base64_decode:\n",
" import base64\n",
" return base64.b64decode(output).decode('utf-8')\n",
" else:\n",
" return output\n",
"\n",
"\n",
"\n",
"print('Common functions defined successfully.')\n",
"\n",
"# Hints for binary (transient fault) retry, (known) error and install guide\n",
"# Hints for tool retry (on transient fault), known errors and install guide\n",
"#\n",
"retry_hints = {'kubectl': ['A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond']}\n",
"error_hints = {'kubectl': [['no such host', 'TSG010 - Get configuration contexts', '../monitor-k8s/tsg010-get-kubernetes-contexts.ipynb'], ['No connection could be made because the target machine actively refused it', 'TSG056 - Kubectl fails with No connection could be made because the target machine actively refused it', '../repair/tsg056-kubectl-no-connection-could-be-made.ipynb']]}\n",
"install_hint = {'kubectl': ['SOP036 - Install kubectl command line interface', '../install/sop036-install-kubectl.ipynb']}"
"retry_hints = {}\n",
"error_hints = {}\n",
"install_hint = {}\n",
"\n",
"\n",
"print('Common functions defined successfully.')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"### Get Postgres server"
]
@@ -400,10 +348,11 @@
"# Sets the 'server' variable to the spec of the Postgres server\n",
"\n",
"import math\n",
"import json\n",
"\n",
"# If a server was provided, get it\n",
"if namespace and name and version:\n",
" server = json.loads(run(f'kubectl get postgresql-{version} -n {namespace} {name} -o json', return_output=True))\n",
"if namespace and name:\n",
" server = json.loads(run(f'kubectl get postgresqls -n {namespace} {name} -o json', return_output=True))\n",
"else:\n",
" # Otherwise prompt the user to select a server\n",
" servers = json.loads(run(f'kubectl get postgresqls --all-namespaces -o json', return_output=True))['items']\n",
@@ -415,19 +364,18 @@
"\n",
" pad = math.floor(math.log10(len(servers)) + 1) + 3\n",
" for i, s in enumerate(servers):\n",
" print(f'{f\"[{i+1}]\":<{pad}}{full_name(s)}')\n",
" print(f'{f\"[{i+1}]\":\u003c{pad}}{full_name(s)}')\n",
"\n",
" while True:\n",
" try:\n",
" i = int(input('Enter the index of a server to troubleshoot: '))\n",
" i = int(input('Enter the index of a server'))\n",
" except ValueError:\n",
" continue\n",
"\n",
" if i >= 1 and i <= len(servers):\n",
" if i \u003e= 1 and i \u003c= len(servers):\n",
" server = servers[i-1]\n",
" namespace = server['metadata']['namespace']\n",
" name = server['metadata']['name']\n",
" version = server['kind'][len('postgresql-'):]\n",
" break\n",
"\n",
"display(Markdown(f'#### Got server {namespace}.{name}'))"
@@ -435,7 +383,11 @@
},
{
"cell_type": "markdown",
"metadata": {},
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"### Summarize all resources"
]
@@ -443,13 +395,15 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"uid = server['metadata']['uid']\n",
"\n",
"display(Markdown(f'#### Server summary'))\n",
"run(f'kubectl get postgresql-{version} -n {namespace} {name}')\n",
"run(f'kubectl get postgresqls -n {namespace} {name}')\n",
"\n",
"display(Markdown(f'#### Resource summary'))\n",
"run(f'kubectl get sts,pods,pvc,svc,ep -n {namespace} -l postgresqls.arcdata.microsoft.com/cluster-id={uid}')"
@@ -457,7 +411,11 @@
},
{
"cell_type": "markdown",
"metadata": {},
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"### Troubleshoot the server"
]
@@ -465,16 +423,22 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"display(Markdown(f'#### Troubleshooting server {namespace}.{name}'))\n",
"run(f'kubectl describe postgresql-{version} -n {namespace} {name}')"
"run(f'kubectl describe postgresqls -n {namespace} {name}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"### Troubleshoot the pods"
]
@@ -482,7 +446,9 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"pods = json.loads(run(f'kubectl get pods -n {namespace} -l postgresqls.arcdata.microsoft.com/cluster-id={uid} -o json', return_output=True))['items']\n",
@@ -505,7 +471,11 @@
},
{
"cell_type": "markdown",
"metadata": {},
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"### Troubleshoot the containers"
]
@@ -513,7 +483,9 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"# Summarize and get logs from each container\n",
@@ -521,7 +493,7 @@
" pod_name = pod['metadata']['name']\n",
" cons = pod['spec']['containers']\n",
" con_statuses = pod['status'].get('containerStatuses', [])\n",
" display(Markdown(f'#### Troubleshooting {len(cons)} container{\"\" if len(cons) < 2 else \"s\"} '\n",
" display(Markdown(f'#### Troubleshooting {len(cons)} container{\"\" if len(cons) \u003c 2 else \"s\"} '\n",
" f'containers for pod {namespace}.{pod_name}'))\n",
"\n",
" for i, con in enumerate(cons):\n",
@@ -537,14 +509,18 @@
" run(f'kubectl logs -n {namespace} {pod_name} {con_name} --tail {tail_lines}')\n",
"\n",
" # Get logs from the previous terminated container if one exists\n",
" if con_restarts > 0:\n",
" if con_restarts \u003e 0:\n",
" display(Markdown(f'#### Logs from previous terminated container {namespace}.{pod_name}/{con_name}'))\n",
" run(f'kubectl logs -n {namespace} {pod_name} {con_name} --tail {tail_lines} --previous')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"### Troubleshoot the PersistentVolumeClaims"
]
@@ -552,7 +528,9 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"display(Markdown(f'#### Troubleshooting PersistentVolumeClaims'))\n",
@@ -562,10 +540,12 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"print('Notebook execution complete.')"
"print(\"Notebook execution is complete.\")"
]
}
],
@@ -576,20 +556,36 @@
"name": "python3",
"display_name": "Python 3"
},
"azdata": {
"pansop": {
"related": "",
"test": {
"ci": false,
"gci": false
},
"contract": {
"requires": {
"kubectl": {
"installed": true
}
"strategy": "",
"types": null,
"disable": {
"reason": "",
"workitems": null,
"types": null
}
},
"side_effects": false
}
"target": {
"current": "public",
"final": "public"
},
"internal": {
"parameters": null,
"symlink": false
},
"timeout": "0"
},
"language_info": {
"codemirror_mode": "{ Name: \"\", Version: \"\"}",
"file_extension": "",
"mimetype": "",
"name": "",
"nbconvert_exporter": "",
"pygments_lexer": "",
"version": ""
},
"widgets": []
}
}

View File

@@ -1,5 +0,0 @@
# Azure Arc Data Services Jupyter Book
## Chapters
1. [Postgres](postgres/readme.md) - notebooks for troubleshooting Postgres on Azure Arc.

View File

@@ -132,13 +132,7 @@
" sys.exit(f'Password is required.')\n",
" confirm_password = getpass.getpass(prompt = 'Confirm password')\n",
" if arc_admin_password != confirm_password:\n",
" sys.exit(f'Passwords do not match.')\n",
"\n",
"os.environ[\"SPN_CLIENT_ID\"] = sp_client_id\n",
"os.environ[\"SPN_TENANT_ID\"] = sp_tenant_id\n",
"if \"AZDATA_NB_VAR_SP_CLIENT_SECRET\" in os.environ:\n",
" os.environ[\"SPN_CLIENT_SECRET\"] = os.environ[\"AZDATA_NB_VAR_SP_CLIENT_SECRET\"]\n",
"os.environ[\"SPN_AUTHORITY\"] = \"https://login.microsoftonline.com\""
" sys.exit(f'Passwords do not match.')"
],
"metadata": {
"azdata_cell_guid": "e7e10828-6cae-45af-8c2f-1484b6d4f9ac",
@@ -188,7 +182,7 @@
"os.environ[\"AZDATA_PASSWORD\"] = arc_admin_password\n",
"if os.name == 'nt':\n",
" print(f'If you don\\'t see output produced by azdata, you can run the following command in a terminal window to check the deployment status:\\n\\t {os.environ[\"AZDATA_NB_VAR_KUBECTL\"]} get pods -n {arc_data_controller_namespace}')\n",
"run_command(f'azdata arc dc create --connectivity-mode {arc_data_controller_connectivity_mode} -n {arc_data_controller_name} -ns {arc_data_controller_namespace} -s {arc_subscription} -g {arc_resource_group} -l {arc_data_controller_location} -sc {arc_data_controller_storage_class} --profile-name {arc_profile}')\n",
"run_command(f'azdata arc dc create --connectivity-mode Indirect -n {arc_data_controller_name} -ns {arc_data_controller_namespace} -s {arc_subscription} -g {arc_resource_group} -l {arc_data_controller_location} -sc {arc_data_controller_storage_class} --profile-name {arc_profile}')\n",
"print(f'Azure Arc Data Controller: {arc_data_controller_name} created.') "
],
"metadata": {

View File

@@ -138,7 +138,7 @@
"\n",
"os.environ[\"AZDATA_USERNAME\"] = sql_username\n",
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_SQL_PASSWORD\"]\n",
"cmd = f'azdata arc sql mi create -n {sql_instance_name} -scd {sql_storage_class_data} -scl {sql_storage_class_logs}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}'\n",
"cmd = f'azdata arc sql mi create -n {sql_instance_name} -scd {sql_storage_class_data} -scl {sql_storage_class_logs} --replicas {sql_replicas}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}'\n",
"out=run_command()"
],
"metadata": {

View File

@@ -2,14 +2,14 @@
"name": "arc",
"displayName": "%arc.displayName%",
"description": "%arc.description%",
"version": "0.7.2",
"version": "0.9.3",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
"icon": "images/extension.png",
"engines": {
"vscode": "*",
"azdata": ">=1.26.0"
"azdata": ">=1.28.0"
},
"activationEvents": [
"onCommand:arc.connectToController",
@@ -132,7 +132,7 @@
],
"resourceDeploymentTypes": [
{
"name": "arc.control.create",
"name": "arc-controller",
"displayName": "%resource.type.azure.arc.display.name%",
"description": "%resource.type.azure.arc.description%",
"platforms": "*",
@@ -144,6 +144,7 @@
],
"providers": [
{
"name": "arc-controller",
"notebookWizard": {
"notebook": "./notebooks/arcDeployment/deploy.arc.data.controller.ipynb",
"type": "new-arc-control-plane",
@@ -240,99 +241,6 @@
]
}
]
},
{
"title": "%arc.data.controller.connectivitymode%",
"fields": [
{
"type": "readonly_text",
"label": "%arc.data.controller.connectivitymode.description%",
"labelWidth": "600px"
},
{
"type": "options",
"label": "%arc.data.controller.connectivitymode%",
"required": true,
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE",
"options": {
"values": [
{
"name": "indirect",
"displayName": "%arc.data.controller.indirect%"
},
{
"name": "direct",
"displayName": "%arc.data.controller.direct%"
}
],
"defaultValue": "%arc.data.controller.indirect%",
"optionsType": "radio"
}
},
{
"type": "readonly_text",
"label": "%arc.data.controller.serviceprincipal.description%",
"labelWidth": "600px",
"links": [
{
"text": "%arc.data.controller.readmore%",
"url": "https://docs.microsoft.com/azure/azure-arc/data/upload-metrics"
}
]
},
{
"label": "%arc.data.controller.spclientid%",
"description": "%arc.data.controller.spclientid.description%",
"variableName": "AZDATA_NB_VAR_SP_CLIENT_ID",
"type": "text",
"required": true,
"defaultValue": "",
"placeHolder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"enabled": {
"target": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE",
"value": "direct"
},
"validations": [
{
"type": "regex_match",
"regex": "^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$",
"description": "%arc.data.controller.spclientid.validation.description%"
}
]
},
{
"label": "%arc.data.controller.spclientsecret%",
"description": "%arc.data.controller.spclientsecret.description%",
"variableName": "AZDATA_NB_VAR_SP_CLIENT_SECRET",
"type": "password",
"required": true,
"defaultValue": "",
"enabled": {
"target": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE",
"value": "direct"
}
},
{
"label": "%arc.data.controller.sptenantid%",
"description": "%arc.data.controller.sptenantid.description%",
"variableName": "AZDATA_NB_VAR_SP_TENANT_ID",
"type": "text",
"required": true,
"defaultValue": "",
"enabled": false,
"valueProvider": {
"providerId": "subscription-id-to-tenant-id",
"triggerField": "AZDATA_NB_VAR_ARC_SUBSCRIPTION"
},
"validations": [
{
"type": "regex_match",
"regex": "^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$",
"description": "%arc.data.controller.sptenantid.validation.description%"
}
]
}
]
}
]
},
@@ -599,12 +507,6 @@
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME)"
},
{
"label": "%arc.data.controller.connectivitymode%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE)"
}
]
}
@@ -618,7 +520,7 @@
},
{
"name": "azdata",
"version": "20.2.6"
"version": "20.3.2"
}
],
"when": true
@@ -626,7 +528,7 @@
]
},
{
"name": "arc.postgres",
"name": "arc-postgres",
"displayName": "%resource.type.arc.postgres.display.name%",
"description": "%resource.type.arc.postgres.description%",
"platforms": "*",
@@ -637,6 +539,7 @@
],
"providers": [
{
"name": "arc-postgres",
"notebookWizard": {
"notebook": "./notebooks/arcDeployment/deploy.postgres.existing.arc.ipynb",
"doneAction": {
@@ -661,7 +564,7 @@
"fields": [
{
"label": "%arc.controller%",
"variableName": "",
"variableName": "CONTROLLER_NAME",
"type": "options",
"editable": false,
"required": true,
@@ -869,7 +772,7 @@
},
{
"name": "azdata",
"version": "20.2.6"
"version": "20.3.2"
}
],
"when": "true"
@@ -912,6 +815,7 @@
"SQL Server"
],
"provider": {
"name": "azure-sql-mi_arc-mi",
"notebookWizard": {
"notebook": "./notebooks/arcDeployment/deploy.sql.existing.arc.ipynb",
"doneAction": {
@@ -936,7 +840,7 @@
"fields": [
{
"label": "%arc.controller%",
"variableName": "",
"variableName": "CONTROLLER_NAME",
"type": "options",
"editable": false,
"required": true,
@@ -996,6 +900,21 @@
{
"title": "%arc.sql.instance.settings.section.title%",
"fields": [
{
"type": "options",
"label": "%arc.sql.replicas.label%",
"description": "%arc.sql.replicas.description%",
"required": true,
"variableName": "AZDATA_NB_VAR_SQL_REPLICAS",
"options": {
"values": [
"1",
"3"
],
"defaultValue": "1",
"optionsType": "radio"
}
},
{
"label": "%arc.storage-class.data.label%",
"description": "%arc.sql.storage-class.data.description%",
@@ -1082,7 +1001,7 @@
},
{
"name": "azdata",
"version": "20.2.6"
"version": "20.3.2"
}
],
"when": "mi-type=arc-mi"
@@ -1112,6 +1031,11 @@
"when": "mi-type=arc-mi"
}
}
],
"resourceDeploymentOptionsSources": [
{
"id": "arc.controllers"
}
]
},
"dependencies": {
@@ -1127,7 +1051,7 @@
"@types/sinon": "^9.0.4",
"@types/uuid": "^8.3.0",
"@types/yamljs": "^0.2.31",
"@microsoft/azdata-test": "^1.4.0",
"@microsoft/azdata-test": "^1.5.0",
"mocha": "^5.2.0",
"mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7",

View File

@@ -22,8 +22,7 @@
"arc.data.controller.cluster.config.profile": "Config profile",
"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 and Connectivity Configuration",
"arc.data.controller.connectivitymode.description": "Select the connectivity mode for the controller.",
"arc.data.controller.create.azureconfig.title": "Azure Configuration",
"arc.data.controller.create.controllerconfig.title": "Controller Configuration",
"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.",
@@ -38,18 +37,6 @@
"arc.data.controller.admin.account.name": "Data controller login",
"arc.data.controller.admin.account.password": "Password",
"arc.data.controller.admin.account.confirm.password": "Confirm password",
"arc.data.controller.connectivitymode": "Connectivity Mode",
"arc.data.controller.direct": "Direct",
"arc.data.controller.indirect": "Indirect",
"arc.data.controller.serviceprincipal.description": "When deploying a controller in direct connected mode a Service Principal is required for connecting to Azure. {0} about how to create this Service Principal and assign it the correct roles.",
"arc.data.controller.spclientid": "Service Principal Client ID",
"arc.data.controller.spclientid.description": "The Client (application) ID of the created Service Principal",
"arc.data.controller.spclientid.validation.description": "The Client ID must be a GUID in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"arc.data.controller.spclientsecret": "Service Principal Client Secret",
"arc.data.controller.spclientsecret.description": "The secret (password) of the Service Principal",
"arc.data.controller.sptenantid": "Service Principal Tenant ID",
"arc.data.controller.sptenantid.description": "The Tenant ID of the Service Principal. This must be the same as the Tenant ID of the subscription selected to create this controller for.",
"arc.data.controller.sptenantid.validation.description": "The tenant ID must be a GUID in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"arc.data.controller.create.summary.title": "Review your configuration",
"arc.data.controller.summary.arc.data.controller": "Azure Arc data controller",
"arc.data.controller.summary.estimated.cost.per.month": "Estimated cost per month",
@@ -97,6 +84,8 @@
"arc.sql.invalid.instance.name": "Instance name must consist of lower case alphanumeric characters or '-', start with a letter, end with an alphanumeric character, and be 13 characters or fewer in length.",
"arc.storage-class.dc.label": "Storage Class",
"arc.sql.storage-class.dc.description": "The storage class to be used for all data and logs persistent volumes for all data controller pods that require them.",
"arc.sql.replicas.label": "Replicas",
"arc.sql.replicas.description": "The number of SQL Managed Instance replicas that will be deployed in your Kubernetes cluster for high availability purposes",
"arc.storage-class.data.label": "Storage Class (Data)",
"arc.sql.storage-class.data.description": "The storage class to be used for data (.mdf)",
"arc.postgres.storage-class.data.description": "The storage class to be used for data persistent volumes",

View File

@@ -10,6 +10,7 @@ import * as loc from '../localizedConstants';
import { throwUnless } from './utils';
export interface KubeClusterContext {
name: string;
namespace?: string;
isCurrentContext: boolean;
}
@@ -18,7 +19,7 @@ export interface KubeClusterContext {
*
* @param configFile
*/
export function getKubeConfigClusterContexts(configFile: string): Promise<KubeClusterContext[]> {
export function getKubeConfigClusterContexts(configFile: string): KubeClusterContext[] {
const config: any = yamljs.load(configFile);
const rawContexts = <any[]>config['contexts'];
throwUnless(rawContexts && rawContexts.length, loc.noContextFound(configFile));
@@ -26,16 +27,16 @@ export function getKubeConfigClusterContexts(configFile: string): Promise<KubeCl
throwUnless(currentContext, loc.noCurrentContextFound(configFile));
const contexts: KubeClusterContext[] = [];
rawContexts.forEach(rawContext => {
const name = <string>rawContext['name'];
const name = rawContext.name as string;
const namespace = rawContext.context.namespace as string;
throwUnless(name, loc.noNameInContext(configFile));
if (name) {
contexts.push({
name: name,
isCurrentContext: name === currentContext
});
}
contexts.push({
name: name,
namespace: namespace,
isCurrentContext: name === currentContext
});
});
return Promise.resolve(contexts);
return contexts;
}
/**
@@ -47,22 +48,23 @@ export function getKubeConfigClusterContexts(configFile: string): Promise<KubeCl
*
*
* @param clusterContexts
* @param previousClusterContext
* @param previousClusterContextName
* @param throwIfNotFound
*/
export function getCurrentClusterContext(clusterContexts: KubeClusterContext[], previousClusterContext?: string, throwIfNotFound: boolean = false): string {
if (previousClusterContext) {
if (clusterContexts.find(c => c.name === previousClusterContext)) { // if previous cluster context value is found in clusters then return that value
export function getCurrentClusterContext(clusterContexts: KubeClusterContext[], previousClusterContextName?: string, throwIfNotFound: boolean = false): KubeClusterContext {
if (previousClusterContextName) {
const previousClusterContext = clusterContexts.find(c => c.name === previousClusterContextName);
if (previousClusterContext) { // if previous cluster context value is found in clusters then return that value
return previousClusterContext;
} else {
if (throwIfNotFound) {
throw new Error(loc.clusterContextNotFound(previousClusterContext));
throw new Error(loc.clusterContextNotFound(previousClusterContextName));
}
}
}
// if not previousClusterContext or throwIfNotFound was false when previousCLusterContext was not found in the clusterContexts
const currentClusterContext = clusterContexts.find(c => c.isCurrentContext)?.name;
const currentClusterContext = clusterContexts.find(c => c.isCurrentContext);
throwUnless(currentClusterContext !== undefined, loc.noCurrentClusterContext);
return currentClusterContext;
}

View File

@@ -6,13 +6,13 @@
/**
* Deferred promise
*/
export class Deferred<T> {
export class Deferred<T = void> {
promise: Promise<T>;
resolve!: (value?: T | PromiseLike<T>) => void;
resolve!: (value: T | PromiseLike<T>) => void;
reject!: (reason?: any) => void;
constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this.resolve = <any>resolve;
this.resolve = resolve;
this.reject = reject;
});
}

View File

@@ -109,7 +109,7 @@ export function getDatabaseStateDisplayText(state: string): string {
* @returns Promise resolving to the user's input if it passed validation,
* or undefined if the input box was closed for any other reason
*/
async function promptInputBox(title: string, options: vscode.InputBoxOptions): Promise<string> {
async function promptInputBox(title: string, options: vscode.InputBoxOptions): Promise<string | undefined> {
const inputBox = vscode.window.createInputBox();
inputBox.title = title;
inputBox.prompt = options.prompt;
@@ -118,7 +118,7 @@ async function promptInputBox(title: string, options: vscode.InputBoxOptions): P
inputBox.value = options.value ?? '';
inputBox.ignoreFocusOut = options.ignoreFocusOut ?? false;
return new Promise<any>(resolve => {
return new Promise(resolve => {
let valueAccepted = false;
inputBox.onDidAccept(async () => {
if (options.validateInput) {
@@ -184,6 +184,30 @@ export async function promptAndConfirmPassword(validate: (input: string) => stri
return undefined;
}
export function generateGuid(): string {
let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
// c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
let oct: string = '';
let tmp: number;
/* tslint:disable:no-bitwise */
for (let a: number = 0; a < 4; a++) {
tmp = (4294967296 * Math.random()) | 0;
oct += hexValues[tmp & 0xF] +
hexValues[tmp >> 4 & 0xF] +
hexValues[tmp >> 8 & 0xF] +
hexValues[tmp >> 12 & 0xF] +
hexValues[tmp >> 16 & 0xF] +
hexValues[tmp >> 20 & 0xF] +
hexValues[tmp >> 24 & 0xF] +
hexValues[tmp >> 28 & 0xF];
}
// 'Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively'
let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0];
return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12);
/* tslint:enable:no-bitwise */
}
/**
* Gets the message to display for a given error object that may be a variety of types.
* @param error The error object
@@ -198,12 +222,16 @@ export function getErrorMessage(error: any, useMessageWithLink: boolean = false)
/**
* Parses an address into its separate ip and port values. Address must be in the form <ip>:<port>
* or <ip>,<port>
* @param address The address to parse
*/
export function parseIpAndPort(address: string): { ip: string, port: string } {
const sections = address.split(':');
let sections = address.split(':');
if (sections.length !== 2) {
throw new Error(`Invalid address format for ${address}. Address must be in the form <ip>:<port>`);
sections = address.split(',');
if (sections.length !== 2) {
throw new Error(`Invalid address format for ${address}. Address must be in the form <ip>:<port> or <ip>,<port>`);
}
}
return {
ip: sections[0],

View File

@@ -45,7 +45,8 @@ export class IconPathHelper {
public static discard: IconPath;
public static fail: IconPath;
public static information: IconPath;
public static gear: IconPath;
public static gearBlue: IconPath;
public static gearGray: IconPath;
public static setExtensionContext(context: vscode.ExtensionContext) {
IconPathHelper.context = context;
@@ -141,9 +142,13 @@ export class IconPathHelper {
light: context.asAbsolutePath('images/information.svg'),
dark: context.asAbsolutePath('images/information.svg'),
};
IconPathHelper.gear = {
light: context.asAbsolutePath('images/gear.svg'),
dark: context.asAbsolutePath('images/gear.svg'),
IconPathHelper.gearBlue = {
light: context.asAbsolutePath('images/gear-colored-blue.svg'),
dark: context.asAbsolutePath('images/gear-colored-blue.svg'),
};
IconPathHelper.gearGray = {
light: context.asAbsolutePath('images/gear-colored-gray.svg'),
dark: context.asAbsolutePath('images/gear-colored-gray.svg'),
};
}
}

View File

@@ -24,7 +24,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<arc.IE
vscode.window.registerTreeDataProvider('azureArc', treeDataProvider);
vscode.commands.registerCommand('arc.createController', async () => {
await vscode.commands.executeCommand('azdata.resource.deploy', 'arc.control.create', ['arc.control.create']);
await vscode.commands.executeCommand('azdata.resource.deploy', 'arc-controller', ['arc-controller']);
});
vscode.commands.registerCommand('arc.connectToController', async () => {

View File

@@ -24,6 +24,8 @@ export const settings = localize('arc.settings', "Settings");
export const security = localize('arc.security', "Security");
export const computeAndStorage = localize('arc.computeAndStorage', "Compute + Storage");
export const nodeParameters = localize('arc.nodeParameters', "Node Parameters");
export const coordinatorNodeParameters = localize('arc.coordinatorNodeParameters', "Coordinator Node Parameters");
export const workerNodeParameters = localize('arc.workerNodeParameters', "Worker Node Parameters");
export const compute = localize('arc.compute', "Compute");
export const backup = localize('arc.backup', "Backup");
export const newSupportRequest = localize('arc.newSupportRequest', "New support request");
@@ -66,7 +68,9 @@ export const feedback = localize('arc.feedback', "Feedback");
export const selectConnectionString = localize('arc.selectConnectionString', "Select from available client connection strings below.");
export const addingWorkerNodes = localize('arc.addingWorkerNodes', "adding worker nodes");
export const workerNodesDescription = localize('arc.workerNodesDescription', "Expand your server group and scale your database by adding worker nodes.");
export const postgresConfigurationInformation = localize('arc.postgres.configurationInformation', "You can configure the number of CPU cores and storage size that will apply to both worker nodes and coordinator node. Each worker node will have the same configuration. Adjust the number of CPU cores and memory settings for your server group.");
export const postgresConfigurationInformation = localize('arc.postgres.configurationInformation', "You can configure the number of CPU cores and storage size that will apply to both worker nodes and coordinator node. Each worker node will have the same configuration. Adjust the number of CPU cores and memory settings for your server group. To reset the requests and/or limits, pass in empty value.");
export const workerNodesConfigurationInformation = localize('arc.workerNodesConfigurationInformation', "You can configure the number of CPU cores and storage size that will apply to all worker nodes. Adjust the number of CPU cores and memory settings for your server group. To reset the requests and/or limits, pass in empty value.");
export const coordinatorNodeConfigurationInformation = localize('arc.coordinatorNodeConfigurationInformation', "You can configure the number of CPU cores and storage size that will apply to the coordinator node. Adjust the number of CPU cores and memory settings for your server group. To reset the requests and/or limits, pass in empty value.");
export const workerNodesInformation = localize('arc.workerNodeInformation', "In preview it is not possible to reduce the number of worker nodes. Please refer to documentation linked above for more information.");
export const vCores = localize('arc.vCores', "vCores");
export const ram = localize('arc.ram', "RAM");
@@ -75,7 +79,10 @@ export const resetAllToDefault = localize('arc.resetAllToDefault', "Reset all to
export const resetToDefault = localize('arc.resetToDefault', "Reset to default");
export const troubleshoot = localize('arc.troubleshoot', "Troubleshoot");
export const clickTheNewSupportRequestButton = localize('arc.clickTheNewSupportRequestButton', "Click the new support request button to file a support request in the Azure Portal.");
export const supportRequestNote = localize('arc.supportRequestNote', "Note that the resource configuration must have been uploaded to Azure first in order to open a support request.");
export const running = localize('arc.running', "Running");
export const ready = localize('arc.ready', "Ready");
export const notReady = localize('arc.notReady', "Not Ready");
export const pending = localize('arc.pending', "Pending");
export const failed = localize('arc.failed', "Failed");
export const unknown = localize('arc.unknown', "Unknown");
@@ -90,13 +97,18 @@ export function connectToMSSql(name: string): string { return localize('arc.conn
export function connectToPGSql(name: string): string { return localize('arc.connectToPGSql', "Connect to PostgreSQL Hyperscale - Azure Arc ({0})", name); }
export const passwordToController = localize('arc.passwordToController', "Provide Password to Controller");
export const controllerUrl = localize('arc.controllerUrl', "Controller URL");
export const controllerUrlPlaceholder = localize('arc.controllerUrlPlaceholder', "https://<IP or hostname>:<port>");
export const controllerUrlDescription = localize('arc.controllerUrlDescription', "The Controller URL is necessary if there are multiple clusters with the same namespace - this should generally not be necessary.");
export const serverEndpoint = localize('arc.serverEndpoint', "Server Endpoint");
export const controllerName = localize('arc.controllerName', "Name");
export const controllerNameDescription = localize('arc.controllerNameDescription', "The name to display in the tree view, this is not applied to the controller itself.");
export const controllerKubeConfig = localize('arc.controllerKubeConfig', "Kube Config File Path");
export const controllerClusterContext = localize('arc.controllerClusterContext', "Cluster Context");
export const defaultControllerName = localize('arc.defaultControllerName', "arc-dc");
export const postgresProviderName = localize('arc.postgresProviderName', "PGSQL");
export const miaaProviderName = localize('arc.miaaProviderName', "MSSQL");
export const controllerUsername = localize('arc.controllerUsername', "Controller Username");
export const controllerPassword = localize('arc.controllerPassword', "Controller Password");
export const username = localize('arc.username', "Username");
export const password = localize('arc.password', "Password");
export const rememberPassword = localize('arc.rememberPassword', "Remember Password");
@@ -128,12 +140,16 @@ export const postgresArcProductName = localize('arc.postgresArcProductName', "Az
export const coordinator = localize('arc.coordinator', "Coordinator");
export const worker = localize('arc.worker', "Worker");
export const monitor = localize('arc.monitor', "Monitor");
export const available = localize('arc.available', "Available");
export const issuesDetected = localize('arc.issuesDetected', "Issues Detected");
export const newDatabase = localize('arc.newDatabase', "New Database");
export const databaseName = localize('arc.databaseName', "Database name");
export const enterNewPassword = localize('arc.enterNewPassword', "Enter a new password");
export const confirmNewPassword = localize('arc.confirmNewPassword', "Confirm the new password");
export const learnAboutPostgresClients = localize('arc.learnAboutPostgresClients', "Learn more about Azure PostgreSQL Hyperscale client interfaces");
export const nodeParametersDescription = localize('arc.nodeParametersDescription', " These server parameters of the Coordinator node and the Worker nodes can be set to custom (non-default) values. Search to find parameters.");
export const coordinatorNodeParametersDescription = localize('arc.coordinatorNodeParametersDescription', " These server parameters of the Coordinator node can be set to custom (non-default) values. Search to find parameters.");
export const workerNodesParametersDescription = localize('arc.workerNodesParametersDescription', " These server parameters of the Worker nodes can be set to custom (non-default) values. Search to find parameters.");
export const learnAboutNodeParameters = localize('arc.learnAboutNodeParameters', "Learn more about database engine settings for Azure Arc enabled PostgreSQL Hyperscale");
export const noNodeParametersFound = localize('arc.noNodeParametersFound', "No worker server parameters found...");
export const searchToFilter = localize('arc.searchToFilter', "Search to filter items...");
@@ -144,35 +160,41 @@ export const postgresComputeAndStorageDescriptionPartTwo = localize('arc.postgre
export const computeAndStorageDescriptionPartThree = localize('arc.computeAndStorageDescriptionPartThree', "without downtime and by");
export const computeAndStorageDescriptionPartFour = localize('arc.computeAndStorageDescriptionPartFour', "Before doing so, you need to ensure");
export const computeAndStorageDescriptionPartFive = localize('arc.computeAndStorageDescriptionPartFive', "there are sufficient resources available");
export const resourceHealthDescription = localize('arc.resourceHealthDescription', "Resource health can tell you if your resource is running as expected.");
export const computeAndStorageDescriptionPartSix = localize('arc.computeAndStorageDescriptionPartSix', "in your Kubernetes cluster to honor this configuration.");
export const node = localize('arc.node', "node");
export const nodes = localize('arc.nodes', "nodes");
export const workerNodes = localize('arc.workerNodes', "Worker Nodes");
export const coordinatorNode = localize('arc.coordinatorNode', "Coordinator Node");
export const storagePerNode = localize('arc.storagePerNode', "storage per node");
export const workerNodeCount = localize('arc.workerNodeCount', "Worker node count:");
export const configurationPerNode = localize('arc.configurationPerNode', "Configuration (per node)");
export const configuration = localize('arc.configurationCoordinatorNode', "Configuration");
export const coresLimit = localize('arc.coresLimit', "CPU limit:");
export const coresRequest = localize('arc.coresRequest', "CPU request:");
export const memoryLimit = localize('arc.memoryLimit', "Memory limit (in GB):");
export const memoryRequest = localize('arc.memoryRequest', "Memory request (in GB):");
export const workerValidationErrorMessage = localize('arc.workerValidationErrorMessage', "The number of workers cannot be decreased.");
export const memoryRequestValidationErrorMessage = localize('arc.memoryRequestValidationErrorMessage', "Memory request must be at least 0.25Gib");
export const memoryLimitValidationErrorMessage = localize('arc.memoryLimitValidationErrorMessage', "Memory limit must be at least 0.25Gib");
export const arcResources = localize('arc.arcResources', "Azure Arc Resources");
export const enterANonEmptyPassword = localize('arc.enterANonEmptyPassword', "Enter a non empty password or press escape to exit.");
export const thePasswordsDoNotMatch = localize('arc.thePasswordsDoNotMatch', "The passwords do not match. Confirm the password or press escape to exit.");
export const passwordReset = localize('arc.passwordReset', "Password reset successfully");
export const podOverview = localize('arc.podOverview', "Pod overview");
export const condition = localize('arc.condition', "Condition");
export const details = localize('arc.details', "Details");
export const lastUpdated = localize('arc.lastUpdated', "Last updated");
export const lastTransition = localize('arc.lastTransition', "Last transition");
export const noExternalEndpoint = localize('arc.noExternalEndpoint', "No External Endpoint has been configured so this information isn't available.");
export const podsReady = localize('arc.podsReady', "pods ready");
export const podsPresent = localize('arc.podsPresent', "Pods Present");
export const podsUsedDescription = localize('arc.podsUsedDescription', "Select a pod in the dropdown below for detailed health information.");
export const connectToPostgresDescription = localize('arc.connectToPostgresDescription', "A connection to the server is required to show and set database engine settings, which will require the PostgreSQL Extension to be installed.");
export const postgresExtension = localize('arc.postgresExtension', "microsoft.azuredatastudio-postgresql");
export const podInitialized = localize('arc.podInitialized', "Pod is initialized.");
export const podReady = localize('arc.podReady', "Pod is ready.");
export const noPodIssuesDetected = localize('arc.noPodIssuesDetected', "There arent any known issues affecting this PostgreSQL Hyperscale instance.");
export const podIssuesDetected = localize('arc.podIssuesDetected', "The pods listed below are experiencing issues that may affect performance or availability.");
export const containerReady = localize('arc.containerReady', "Pod containers are ready.");
export const podScheduled = localize('arc.podScheduled', "Pod is schedulable.");
export function rangeSetting(min: string, max: string): string { return localize('arc.rangeSetting', "Value is expected to be in the range {0} - {1}", min, max); }
export function allowedValue(value: string): string { return localize('arc.allowedValue', "Value is expected to be {0}", value); }
export function databaseCreated(name: string): string { return localize('arc.databaseCreated', "Database {0} created", name); }
export function deletingInstance(name: string): string { return localize('arc.deletingInstance', "Deleting instance '{0}'...", name); }
export function installingExtension(name: string): string { return localize('arc.installingExtension', "Installing extension '{0}'...", name); }
@@ -182,6 +204,9 @@ export function instanceDeleted(name: string): string { return localize('arc.ins
export function instanceUpdated(name: string): string { return localize('arc.instanceUpdated', "Instance '{0}' updated", name); }
export function copiedToClipboard(name: string): string { return localize('arc.copiedToClipboard', "{0} copied to clipboard", name); }
export function clickTheTroubleshootButton(resourceType: string): string { return localize('arc.clickTheTroubleshootButton', "Click the troubleshoot button to open the Azure Arc {0} troubleshooting notebook.", resourceType); }
export function dataStorage(value: string): string { return localize('arc.dataStorage', "{0} data", value); }
export function logStorage(value: string): string { return localize('arc.logStorage', "{0} log", value); }
export function backupsStorage(value: string): string { return localize('arc.backupsStorage', "{0} backups", value); }
export function numVCores(vCores: string | undefined): string {
if (vCores && +vCores > 0) {
if (+vCores === 1) {
@@ -194,13 +219,11 @@ export function numVCores(vCores: string | undefined): string {
}
}
export function updated(when: string): string { return localize('arc.updated', "Updated {0}", when); }
export function validationMin(min: number): string { return localize('arc.validationMin', "Value must be greater than or equal to {0}.", min); }
// Errors
export const pgConnectionRequired = localize('arc.pgConnectionRequired', "A connection is required to show and set database engine settings.");
export const miaaConnectionRequired = localize('arc.miaaConnectionRequired', "A connection is required to list the databases on this instance.");
export const couldNotFindControllerRegistration = localize('arc.couldNotFindControllerRegistration', "Could not find controller registration.");
export function outOfRange(min: string, max: string): string { return localize('arc.outOfRange', "The number must be in range {0} - {1}", min, max); }
export function refreshFailed(error: any): string { return localize('arc.refreshFailed', "Refresh failed. {0}", getErrorMessage(error)); }
export function resetFailed(error: any): string { return localize('arc.resetFailed', "Reset failed. {0}", getErrorMessage(error)); }
export function openDashboardFailed(error: any): string { return localize('arc.openDashboardFailed', "Error opening dashboard. {0}", getErrorMessage(error)); }
@@ -218,6 +241,7 @@ export function fetchEndpointsFailed(name: string, error: any): string { return
export function fetchRegistrationsFailed(name: string, error: any): string { return localize('arc.fetchRegistrationsFailed', "An unexpected error occurred retrieving the registrations for '{0}'. {1}", name, getErrorMessage(error)); }
export function fetchDatabasesFailed(name: string, error: any): string { return localize('arc.fetchDatabasesFailed', "An unexpected error occurred retrieving the databases for '{0}'. {1}", name, getErrorMessage(error)); }
export function fetchEngineSettingsFailed(name: string, error: any): string { return localize('arc.fetchEngineSettingsFailed', "An unexpected error occurred retrieving the engine settings for '{0}'. {1}", name, getErrorMessage(error)); }
export function numberOfIssuesDetected(name: string, issues: number): string { return localize('arc.numberOfIssuesDetected', "• {0} ({1} issues)", name, issues); }
export function instanceDeletionWarning(name: string): string { return localize('arc.instanceDeletionWarning', "Warning! Deleting an instance is permanent and cannot be undone. To delete the instance '{0}' type the name '{0}' below to proceed.", name); }
export function invalidInstanceDeletionName(name: string): string { return localize('arc.invalidInstanceDeletionName', "The value '{0}' does not match the instance name. Try again or press escape to exit", name); }
export function couldNotFindAzureResource(name: string): string { return localize('arc.couldNotFindAzureResource', "Could not find Azure resource for {0}", name); }

View File

@@ -46,6 +46,20 @@ export class ControllerModel {
return this._info;
}
/**
* Gets the controller context to use when executing azdata commands. This is in one of two forms :
*
* If no URL is specified for this controller then just the namespace is used (e.g. test-namespace)
* If a URL is specified then a 3-part name is used, combining the namespace, username and URL separated by
* / (e.g. test-namespace/admin/https://10.91.86.13:30080)
*/
public get controllerContext(): string {
if (this._info.endpoint) {
return `${this._info.namespace}/${this._info.username}/${this._info.endpoint}`;
}
return this._info.namespace;
}
public set info(value: ControllerInfo) {
this._info = value;
this._onInfoUpdated.fire(this._info);
@@ -63,10 +77,10 @@ export class ControllerModel {
* calls from changing the context while commands for this session are being executed.
* @param promptReconnect
*/
public async acquireAzdataSession(promptReconnect: boolean = false): Promise<azdataExt.AzdataSession> {
public async login(promptReconnect: boolean = false): Promise<void> {
let promptForValidClusterContext: boolean = false;
try {
const contexts = await getKubeConfigClusterContexts(this.info.kubeConfigFilePath);
const contexts = getKubeConfigClusterContexts(this.info.kubeConfigFilePath);
getCurrentClusterContext(contexts, this.info.kubeClusterContext, true); // this throws if this.info.kubeClusterContext is not found in 'contexts'
} catch (error) {
const response = await vscode.window.showErrorMessage(loc.clusterContextConfigNoLongerValid(this.info.kubeConfigFilePath, this.info.kubeClusterContext, error), loc.yes, loc.no);
@@ -100,8 +114,7 @@ export class ControllerModel {
}
}
}
return this._azdataApi.azdata.acquireSession(this.info.url, this.info.username, this._password, this.azdataAdditionalEnvVars);
await this._azdataApi.azdata.login({ endpoint: this.info.endpoint, namespace: this.info.namespace }, this.info.username, this._password, this.azdataAdditionalEnvVars);
}
/**
@@ -115,67 +128,64 @@ export class ControllerModel {
await this.refresh(false);
}
}
public async refresh(showErrors: boolean = true, promptReconnect: boolean = false): Promise<void> {
const session = await this.acquireAzdataSession(promptReconnect);
public async refresh(showErrors: boolean = true): Promise<void> {
// First need to log in to ensure that we're able to authenticate with the controller
await this.login(false);
const newRegistrations: Registration[] = [];
try {
await Promise.all([
this._azdataApi.azdata.arc.dc.config.show(this.azdataAdditionalEnvVars, session).then(result => {
this._controllerConfig = result.result;
this.configLastUpdated = new Date();
this._onConfigUpdated.fire(this._controllerConfig);
}).catch(err => {
// If an error occurs show a message so the user knows something failed but still
// fire the event so callers hooking into this can handle the error (e.g. so dashboards don't show the
// loading icon forever)
if (showErrors) {
vscode.window.showErrorMessage(loc.fetchConfigFailed(this.info.name, err));
}
this._onConfigUpdated.fire(this._controllerConfig);
throw err;
await Promise.all([
this._azdataApi.azdata.arc.dc.config.show(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
this._controllerConfig = result.result;
this.configLastUpdated = new Date();
this._onConfigUpdated.fire(this._controllerConfig);
}).catch(err => {
// If an error occurs show a message so the user knows something failed but still
// fire the event so callers hooking into this can handle the error (e.g. so dashboards don't show the
// loading icon forever)
if (showErrors) {
vscode.window.showErrorMessage(loc.fetchConfigFailed(this.info.name, err));
}
this._onConfigUpdated.fire(this._controllerConfig);
throw err;
}),
this._azdataApi.azdata.arc.dc.endpoint.list(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
this._endpoints = result.result;
this.endpointsLastUpdated = new Date();
this._onEndpointsUpdated.fire(this._endpoints);
}).catch(err => {
// If an error occurs show a message so the user knows something failed but still
// fire the event so callers can know to update (e.g. so dashboards don't show the
// loading icon forever)
if (showErrors) {
vscode.window.showErrorMessage(loc.fetchEndpointsFailed(this.info.name, err));
}
this._onEndpointsUpdated.fire(this._endpoints);
throw err;
}),
Promise.all([
this._azdataApi.azdata.arc.postgres.server.list(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
newRegistrations.push(...result.result.map(r => {
return {
instanceName: r.name,
state: r.state,
instanceType: ResourceType.postgresInstances
};
}));
}),
this._azdataApi.azdata.arc.dc.endpoint.list(this.azdataAdditionalEnvVars, session).then(result => {
this._endpoints = result.result;
this.endpointsLastUpdated = new Date();
this._onEndpointsUpdated.fire(this._endpoints);
}).catch(err => {
// If an error occurs show a message so the user knows something failed but still
// fire the event so callers can know to update (e.g. so dashboards don't show the
// loading icon forever)
if (showErrors) {
vscode.window.showErrorMessage(loc.fetchEndpointsFailed(this.info.name, err));
}
this._onEndpointsUpdated.fire(this._endpoints);
throw err;
}),
Promise.all([
this._azdataApi.azdata.arc.postgres.server.list(this.azdataAdditionalEnvVars, session).then(result => {
newRegistrations.push(...result.result.map(r => {
return {
instanceName: r.name,
state: r.state,
instanceType: ResourceType.postgresInstances
};
}));
}),
this._azdataApi.azdata.arc.sql.mi.list(this.azdataAdditionalEnvVars, session).then(result => {
newRegistrations.push(...result.result.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);
this._azdataApi.azdata.arc.sql.mi.list(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
newRegistrations.push(...result.result.map(r => {
return {
instanceName: r.name,
state: r.state,
instanceType: ResourceType.sqlManagedInstances
};
}));
})
]);
} finally {
session.dispose();
}
]).then(() => {
this._registrations = newRegistrations;
this.registrationsLastUpdated = new Date();
this._onRegistrationsUpdated.fire(this._registrations);
})
]);
}
public get endpoints(): azdataExt.DcEndpointListResult[] {
@@ -204,6 +214,6 @@ export class ControllerModel {
* property to for use a display label for this controller
*/
public get label(): string {
return `${this.info.name} (${this.info.url})`;
return `${this.info.name} (${this.controllerContext})`;
}
}

View File

@@ -71,11 +71,9 @@ export class MiaaModel extends ResourceModel {
return this._refreshPromise.promise;
}
this._refreshPromise = new Deferred();
let session: azdataExt.AzdataSession | undefined = undefined;
try {
session = await this.controllerModel.acquireAzdataSession();
try {
const result = await this._azdataApi.azdata.arc.sql.mi.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, session);
const result = await this._azdataApi.azdata.arc.sql.mi.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, this.controllerModel.controllerContext);
this._config = result.result;
this.configLastUpdated = new Date();
this._onConfigUpdated.fire(this._config);
@@ -109,7 +107,6 @@ export class MiaaModel extends ResourceModel {
this._refreshPromise.reject(err);
throw err;
} finally {
session?.dispose();
this._refreshPromise = undefined;
}
}

View File

@@ -28,7 +28,8 @@ export type EngineSettingsModel = {
export class PostgresModel extends ResourceModel {
private _config?: azdataExt.PostgresServerShowResult;
public _engineSettings: EngineSettingsModel[] = [];
public workerNodesEngineSettings: EngineSettingsModel[] = [];
public coordinatorNodeEngineSettings: EngineSettingsModel[] = [];
private readonly _azdataApi: azdataExt.IExtension;
private readonly _onConfigUpdated = new vscode.EventEmitter<azdataExt.PostgresServerShowResult>();
@@ -52,10 +53,7 @@ export class PostgresModel extends ResourceModel {
/** Returns the major version of Postgres */
public get engineVersion(): string | undefined {
const kind = this._config?.kind;
return kind
? kind.substring(kind.lastIndexOf('-') + 1)
: undefined;
return this._config?.spec.engine.version;
}
/** Returns the IP address and port of Postgres */
@@ -75,7 +73,9 @@ export class PostgresModel extends ResourceModel {
const ramLimit = this._config.spec.scheduling?.default?.resources?.limits?.memory;
const cpuRequest = this._config.spec.scheduling?.default?.resources?.requests?.cpu;
const ramRequest = this._config.spec.scheduling?.default?.resources?.requests?.memory;
const storage = this._config.spec.storage?.data?.size;
const dataStorage = this._config.spec.storage?.data?.size;
const logStorage = this._config.spec.storage?.logs?.size;
const backupsStorage = this._config.spec.storage?.backups?.size;
// scale.shards was renamed to scale.workers. Check both for backwards compatibility.
const scale = this._config.spec.scale;
@@ -93,8 +93,19 @@ export class PostgresModel extends ResourceModel {
configuration.push(`${ramLimit ?? ramRequest!} ${loc.ram}`);
}
if (storage) {
configuration.push(`${storage} ${loc.storagePerNode}`);
let storage: string[] = [];
if (dataStorage) {
storage.push(loc.dataStorage(dataStorage));
}
if (logStorage) {
storage.push(loc.logStorage(logStorage));
}
if (backupsStorage) {
storage.push(loc.backupsStorage(backupsStorage));
}
if (dataStorage || logStorage || backupsStorage) {
storage.push(`${loc.storagePerNode}`);
configuration.push(storage.join(' '));
}
return configuration.join(', ');
@@ -107,10 +118,8 @@ export class PostgresModel extends ResourceModel {
return this._refreshPromise.promise;
}
this._refreshPromise = new Deferred();
let session: azdataExt.AzdataSession | undefined = undefined;
try {
session = await this.controllerModel.acquireAzdataSession();
this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, session)).result;
this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, this.controllerModel.controllerContext)).result;
this.configLastUpdated = new Date();
this._onConfigUpdated.fire(this._config);
this._refreshPromise.resolve();
@@ -118,7 +127,6 @@ export class PostgresModel extends ResourceModel {
this._refreshPromise.reject(err);
throw err;
} finally {
session?.dispose();
this._refreshPromise = undefined;
}
}
@@ -137,6 +145,7 @@ export class PostgresModel extends ResourceModel {
this._activeConnectionId = result.connectionId;
}
// TODO Need to make separate calls for worker nodes and coordinator node
const provider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(this._connectionProfile!.providerName, azdata.DataProviderType.QueryProvider);
const ownerUri = await azdata.connection.getUriForConnection(this._activeConnectionId);
@@ -150,7 +159,7 @@ export class PostgresModel extends ResourceModel {
'shared_preload_libraries', 'synchronous_commit', 'ssl', 'unix_socket_permissions', 'wal_level'
];
this._engineSettings = [];
this.workerNodesEngineSettings = [];
engineSettings.rows.forEach(row => {
let rowValues = row.map(c => c.displayValue);
@@ -166,12 +175,12 @@ export class PostgresModel extends ResourceModel {
type: rowValues.shift()
};
this._engineSettings.push(result);
this.workerNodesEngineSettings.push(result);
}
});
this.engineSettingsLastUpdated = new Date();
this._onEngineSettingsUpdated.fire(this._engineSettings);
this._onEngineSettingsUpdated.fire(this.workerNodesEngineSettings);
}
protected createConnectionProfile(): azdata.IConnectionProfile {

View File

@@ -30,7 +30,7 @@ export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourcePro
const controller = (await getRegisteredDataControllers(this._treeProvider)).find(ci => ci.label === controllerLabel);
throwUnless(controller !== undefined, loc.noControllerInfoFound(controllerLabel));
switch (variableName) {
case 'endpoint': return controller.info.url;
case 'endpoint': return controller.info.endpoint || '';
case 'username': return controller.info.username;
case 'kubeConfig': return controller.info.kubeConfigFilePath;
case 'clusterContext': return controller.info.kubeClusterContext;

View File

@@ -51,7 +51,7 @@ describe('KubeUtils', function (): void {
contexts[1].name.should.equal('kubernetes-admin@kubernetes', `test: ${testName} failed`);
contexts[1].isCurrentContext.should.be.false(`test: ${testName} failed`);
};
verifyContexts(await getKubeConfigClusterContexts(configFile), 'getKubeConfigClusterContexts');
verifyContexts(getKubeConfigClusterContexts(configFile), 'getKubeConfigClusterContexts');
});
it('throws error when unable to load config file', async () => {
const error = new Error('unknown error accessing file');

View File

@@ -10,77 +10,81 @@ import * as azdataExt from 'azdata-ext';
*/
export class FakeAzdataApi implements azdataExt.IAzdataApi {
public postgresInstances: azdataExt.PostgresServerListResult[] = [];
public miaaInstances: azdataExt.SqlMiListResult[] = [];
private _arcApi = {
dc: {
create(_namespace: string, _name: string, _connectivityMode: string, _resourceGroup: string, _location: string, _subscription: string, _profileName?: string, _storageClass?: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
endpoint: {
async list(): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> { return <any>{ result: [] }; }
},
config: {
list(): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> { throw new Error('Method not implemented.'); },
async show(): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> { return <any>{ result: undefined! }; }
}
},
postgres: {
server: {
postgresInstances: <azdataExt.PostgresServerListResult[]>[],
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
async list(): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> { return { result: this.postgresInstances, logs: [], stdout: [], stderr: [] }; },
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> { throw new Error('Method not implemented.'); },
edit(
_name: string,
_args: {
adminPassword?: boolean,
coresLimit?: string,
coresRequest?: string,
engineSettings?: string,
extensions?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
port?: number,
replaceEngineSettings?: boolean,
workers?: number
},
_additionalEnvVars?: azdataExt.AdditionalEnvVars
): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
}
},
sql: {
mi: {
miaaInstances: <azdataExt.SqlMiListResult[]>[],
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
async list(): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> { return { logs: [], stdout: [], stderr: [], result: this.miaaInstances }; },
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> { throw new Error('Method not implemented.'); },
edit(
_name: string,
_args: {
coresLimit?: string,
coresRequest?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean
}): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
}
}
};
public set postgresInstances(instances: azdataExt.PostgresServerListResult[]) {
this._arcApi.postgres.server.postgresInstances = instances;
}
public set miaaInstances(instances: azdataExt.SqlMiListResult[]) {
this._arcApi.sql.mi.miaaInstances = instances;
}
//
// API Implementation
//
public get arc() {
const self = this;
return {
dc: {
create(_namespace: string, _name: string, _connectivityMode: string, _resourceGroup: string, _location: string, _subscription: string, _profileName?: string, _storageClass?: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
endpoint: {
async list(): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> { return <any>{ result: [] }; }
},
config: {
list(): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> { throw new Error('Method not implemented.'); },
async show(): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> { return <any>{ result: undefined! }; }
}
},
postgres: {
server: {
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
async list(): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> { return <any>{ result: self.postgresInstances }; },
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> { throw new Error('Method not implemented.'); },
edit(
_name: string,
_args: {
adminPassword?: boolean,
coresLimit?: string,
coresRequest?: string,
engineSettings?: string,
extensions?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
port?: number,
replaceEngineSettings?: boolean,
workers?: number
},
_engineVersion?: string,
_additionalEnvVars?: azdataExt.AdditionalEnvVars
): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
}
},
sql: {
mi: {
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
async list(): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> { return <any>{ result: self.miaaInstances }; },
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> { throw new Error('Method not implemented.'); },
edit(
_name: string,
_args: {
coresLimit?: string,
coresRequest?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean
}): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
}
}
};
return this._arcApi;
}
getPath(): Promise<string> {
throw new Error('Method not implemented.');
}
login(_endpoint: string, _username: string, _password: string): Promise<azdataExt.AzdataOutput<void>> {
login(_endpointOrNamespace: azdataExt.EndpointOrNamespace, _username: string, _password: string, _additionalEnvVars: azdataExt.AdditionalEnvVars = {}, _azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> {
return <any>undefined;
}
acquireSession(_endpoint: string, _username: string, _password: string): Promise<azdataExt.AzdataSession> {
return Promise.resolve({ dispose: () => { } });
}
version(): Promise<azdataExt.AzdataOutput<string>> {
throw new Error('Method not implemented.');
}

View File

@@ -11,7 +11,7 @@ import { AzureArcTreeDataProvider } from '../../ui/tree/azureArcTreeDataProvider
export class FakeControllerModel extends ControllerModel {
constructor(treeDataProvider?: AzureArcTreeDataProvider, info?: Partial<ControllerInfo>, password?: string) {
const _info: ControllerInfo = Object.assign({ id: uuid(), url: '', kubeConfigFilePath: '', kubeClusterContext: '', name: '', username: '', rememberPassword: false, resources: [] }, info);
const _info: ControllerInfo = Object.assign({ id: uuid(), endpoint: '', kubeConfigFilePath: '', kubeClusterContext: '', name: '', namespace: '', username: '', rememberPassword: false, resources: [] }, info);
super(treeDataProvider!, _info, password);
}

View File

@@ -22,6 +22,20 @@ interface ExtensionGlobalMemento extends vscode.Memento {
setKeysForSync(keys: string[]): void;
}
function getDefaultControllerInfo(): ControllerInfo {
return {
id: uuid(),
endpoint: '127.0.0.1',
kubeConfigFilePath: '/path/to/.kube/config',
kubeClusterContext: 'currentCluster',
username: 'admin',
name: 'arc',
namespace: 'arc-ns',
rememberPassword: true,
resources: []
};
}
describe('ControllerModel', function (): void {
afterEach(function (): void {
sinon.restore();
@@ -39,15 +53,15 @@ describe('ControllerModel', function (): void {
beforeEach(function (): void {
sinon.stub(ConnectToControllerDialog.prototype, 'showDialog');
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').resolves([{ name: 'currentCluster', isCurrentContext: true }]);
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').returns([{ name: 'currentCluster', isCurrentContext: true }]);
sinon.stub(vscode.window, 'showErrorMessage').resolves(<any>loc.yes);
});
it('Rejected with expected error when user cancels', async function (): Promise<void> {
// Returning an undefined model here indicates that the dialog closed without clicking "Ok" - usually through the user clicking "Cancel"
sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(undefined));
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
await should(model.acquireAzdataSession()).be.rejectedWith(new UserCancelledError(loc.userCancelledError));
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
await should(model.login()).be.rejectedWith(new UserCancelledError(loc.userCancelledError));
});
it('Reads password from cred store', async function (): Promise<void> {
@@ -62,13 +76,13 @@ describe('ControllerModel', function (): void {
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
await model.acquireAzdataSession();
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
await model.login();
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
it('Prompt for password when not in cred store', async function (): Promise<void> {
@@ -83,18 +97,18 @@ describe('ControllerModel', function (): void {
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
// Set up dialog to return new model with our password
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), password);
sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
await model.acquireAzdataSession();
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
await model.login();
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
it('Prompt for password when rememberPassword is true but prompt reconnect is true', async function (): Promise<void> {
@@ -108,19 +122,19 @@ describe('ControllerModel', function (): void {
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
// Set up dialog to return new model with our new password from the reprompt
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), password);
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
await model.acquireAzdataSession(true);
await model.login(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called');
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
it('Prompt for password when we already have a password but prompt reconnect is true', async function (): Promise<void> {
@@ -134,20 +148,20 @@ describe('ControllerModel', function (): void {
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
// Set up dialog to return new model with our new password from the reprompt
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), password);
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
// Set up original model with a password
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, 'originalPassword');
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), 'originalPassword');
await model.acquireAzdataSession(true);
await model.login(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called');
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
it('Model values are updated correctly when modified during reconnect', async function (): Promise<void> {
@@ -162,7 +176,7 @@ describe('ControllerModel', function (): void {
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
@@ -170,27 +184,19 @@ describe('ControllerModel', function (): void {
const originalPassword = 'originalPassword';
const model = new ControllerModel(
treeDataProvider,
{
id: uuid(),
url: '127.0.0.1',
kubeConfigFilePath: '/path/to/.kube/config',
kubeClusterContext: 'currentCluster',
username: 'admin',
name: 'arc',
rememberPassword: false,
resources: []
},
getDefaultControllerInfo(),
originalPassword
);
await treeDataProvider.addOrUpdateController(model, originalPassword);
const newInfo: ControllerInfo = {
id: model.info.id, // The ID stays the same since we're just re-entering information for the same model
url: 'newUrl',
endpoint: 'newUrl',
kubeConfigFilePath: '/path/to/.kube/config',
kubeClusterContext: 'currentCluster',
username: 'newUser',
name: 'newName',
namespace: 'newNamespace',
rememberPassword: true,
resources: []
};
@@ -203,7 +209,7 @@ describe('ControllerModel', function (): void {
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(
{ controllerModel: newModel, password: newPassword }));
await model.acquireAzdataSession(true);
await model.login(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called');
should((await treeDataProvider.getChildren()).length).equal(1, 'Tree Data provider should still only have 1 node');
should(model.info).deepEqual(newInfo, 'Model info should have been updated');

View File

@@ -0,0 +1,544 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { PGResourceInfo, ResourceType } from 'arc';
import * as azdataExt from 'azdata-ext';
import * as azdata from 'azdata';
import * as should from 'should';
import * as sinon from 'sinon';
import * as TypeMoq from 'typemoq';
import * as vscode from 'vscode';
import { generateGuid } from '../../common/utils';
import { UserCancelledError } from '../../common/api';
import { ControllerModel, Registration } from '../../models/controllerModel';
import { PostgresModel, EngineSettingsModel } from '../../models/postgresModel';
import { ConnectToPGSqlDialog } from '../../ui/dialogs/connectPGDialog';
import { AzureArcTreeDataProvider } from '../../ui/tree/azureArcTreeDataProvider';
import { FakeControllerModel } from '../mocks/fakeControllerModel';
import { FakeAzdataApi } from '../mocks/fakeAzdataApi';
export const FakePostgresServerShowOutput: azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult> = {
logs: [],
stdout: [],
stderr: [],
result: {
apiVersion: 'version',
kind: 'postgresql',
metadata: {
creationTimestamp: '',
generation: 1,
name: 'pgt',
namespace: 'ns',
resourceVersion: '',
selfLink: '',
uid: '',
},
spec: {
engine: {
extensions: [{ name: '' }],
settings: {
default: { ['']: '' }
},
version: ''
},
scale: {
shards: 0,
workers: 0
},
scheduling: {
default: {
resources: {
requests: {
cpu: '',
memory: ''
},
limits: {
cpu: '',
memory: ''
}
}
}
},
service: {
type: '',
port: 0
},
storage: {
data: {
className: '',
size: ''
},
logs: {
className: '',
size: ''
},
backups: {
className: '',
size: ''
}
}
},
status: {
externalEndpoint: '127.0.0.1:5432',
readyPods: '',
state: '',
logSearchDashboard: '',
metricsDashboard: '',
podsStatus: [{
conditions: [{
lastTransitionTime: '',
message: '',
reason: '',
status: '',
type: '',
}],
name: '',
role: '',
}]
}
}
};
describe('PostgresModel', function (): void {
let controllerModel: ControllerModel;
let postgresModel: PostgresModel;
let azdataApi: azdataExt.IAzdataApi;
afterEach(function (): void {
sinon.restore();
});
beforeEach(async () => {
// Setup Controller Model
controllerModel = new FakeControllerModel();
//Stub calling azdata login and acquiring session
sinon.stub(controllerModel, 'login').returns(Promise.resolve());
// Stub the azdata CLI API
azdataApi = new FakeAzdataApi();
const azdataExt = TypeMoq.Mock.ofType<azdataExt.IExtension>();
azdataExt.setup(x => x.azdata).returns(() => azdataApi);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExt.object });
});
describe('refresh', function (): void {
beforeEach(async () => {
// Setup PostgresModel
const postgresResource: PGResourceInfo = { name: 'pgt', resourceType: '' };
const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances };
postgresModel = new PostgresModel(controllerModel, postgresResource, registration, new AzureArcTreeDataProvider(TypeMoq.Mock.ofType<vscode.ExtensionContext>().object));
});
it('Updates model to expected config', async function (): Promise<void> {
const postgresShowStub = sinon.stub(azdataApi.arc.postgres.server, 'show').resolves(FakePostgresServerShowOutput);
await postgresModel.refresh();
sinon.assert.calledOnceWithExactly(postgresShowStub, postgresModel.info.name, sinon.match.any, sinon.match.any);
sinon.assert.match(postgresModel.config, FakePostgresServerShowOutput.result);
});
it('Updates onConfigLastUpdated when model is refreshed', async function (): Promise<void> {
const postgresShowStub = sinon.stub(azdataApi.arc.postgres.server, 'show').resolves(FakePostgresServerShowOutput);
await postgresModel.refresh();
sinon.assert.calledOnceWithExactly(postgresShowStub, postgresModel.info.name, sinon.match.any, sinon.match.any);
should(postgresModel.configLastUpdated).be.Date();
});
it('Calls onConfigUpdated event when model is refreshed', async function (): Promise<void> {
const postgresShowStub = sinon.stub(azdataApi.arc.postgres.server, 'show').resolves(FakePostgresServerShowOutput);
const configUpdatedEvent = sinon.spy(vscode.EventEmitter.prototype, 'fire');
await postgresModel.refresh();
sinon.assert.calledOnceWithExactly(postgresShowStub, postgresModel.info.name, sinon.match.any, sinon.match.any);
sinon.assert.calledOnceWithExactly(configUpdatedEvent, postgresModel.config);
});
it('Expected exception is thrown', async function (): Promise<void> {
// Stub 'azdata arc postgres server show' to throw an exception
const error = new Error('something bad happened');
sinon.stub(azdataApi.arc.postgres.server, 'show').throws(error);
await should(postgresModel.refresh()).be.rejectedWith(error);
});
});
describe('getConnectionProfile', function (): void {
beforeEach(async () => {
// Setup PostgresModel
const postgresResource: PGResourceInfo = { name: 'pgt', resourceType: '', userName: 'postgres', connectionId: '12345678' };
const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances };
postgresModel = new PostgresModel(controllerModel, postgresResource, registration, new AzureArcTreeDataProvider(TypeMoq.Mock.ofType<vscode.ExtensionContext>().object));
sinon.stub(azdataApi.arc.postgres.server, 'show').resolves(FakePostgresServerShowOutput);
//Call to provide external endpoint
await postgresModel.refresh();
});
it('Rejected with expected error when user cancels', async function (): Promise<void> {
const close = sinon.stub(ConnectToPGSqlDialog.prototype, 'waitForClose').returns(Promise.resolve(undefined));
await should(postgresModel['getConnectionProfile']()).be.rejectedWith(new UserCancelledError());
sinon.assert.calledOnce(close);
});
it('Show dialog prompt if password not found', async function (): Promise<void> {
const connect = sinon.stub(azdata.connection, 'connect');
const cancelButtonMock = TypeMoq.Mock.ofType<azdata.window.Button>();
cancelButtonMock.setup((x: any) => x.then).returns(() => undefined);
const dialogMock = TypeMoq.Mock.ofType<azdata.window.Dialog>();
dialogMock.setup(x => x.cancelButton).returns(() => cancelButtonMock.object);
dialogMock.setup((x: any) => x.then).returns(() => undefined);
const show = sinon.stub(azdata.window, 'createModelViewDialog').returns(dialogMock.object);
sinon.stub(azdata.window, 'openDialog');
const iconnectionProfileMock = TypeMoq.Mock.ofType<azdata.IConnectionProfile>();
iconnectionProfileMock.setup((x: any) => x.then).returns(() => undefined);
const close = sinon.stub(ConnectToPGSqlDialog.prototype, 'waitForClose').returns(Promise.resolve(iconnectionProfileMock.object));
await postgresModel['getConnectionProfile']();
sinon.assert.notCalled(connect);
sinon.assert.calledOnce(show);
sinon.assert.calledOnce(close);
});
it('Reads password from cred store and no dialog prompt', async function (): Promise<void> {
const password = generateGuid();
// Set up cred store to return our password
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: password }));
credProviderMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.credentials, 'getProvider').returns(Promise.resolve(credProviderMock.object));
const connectionResultMock = TypeMoq.Mock.ofType<azdata.ConnectionResult>();
connectionResultMock.setup(x => x.connected).returns(() => true);
connectionResultMock.setup((x: any) => x.then).returns(() => undefined);
const connect = sinon.stub(azdata.connection, 'connect').returns(Promise.resolve(connectionResultMock.object));
const cancelButtonMock = TypeMoq.Mock.ofType<azdata.window.Button>();
cancelButtonMock.setup((x: any) => x.then).returns(() => undefined);
const dialogMock = TypeMoq.Mock.ofType<azdata.window.Dialog>();
dialogMock.setup(x => x.cancelButton).returns(() => cancelButtonMock.object);
dialogMock.setup((x: any) => x.then).returns(() => undefined);
const show = sinon.stub(azdata.window, 'createModelViewDialog').returns(dialogMock.object);
sinon.stub(azdata.window, 'openDialog');
const treeSave = sinon.spy(AzureArcTreeDataProvider.prototype, 'saveControllers');
await postgresModel['getConnectionProfile']();
sinon.assert.calledOnce(connect);
sinon.assert.notCalled(show);
sinon.assert.calledOnce(treeSave);
});
it('Reads password from cred store and connect fails, show dialog prompt', async function (): Promise<void> {
const password = generateGuid();
// Set up cred store to return our password
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: password }));
credProviderMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.credentials, 'getProvider').returns(Promise.resolve(credProviderMock.object));
const connectionResultMock = TypeMoq.Mock.ofType<azdata.ConnectionResult>();
connectionResultMock.setup(x => x.connected).returns(() => false);
connectionResultMock.setup((x: any) => x.then).returns(() => undefined);
const connect = sinon.stub(azdata.connection, 'connect').returns(Promise.resolve(connectionResultMock.object));
const iconnectionProfileMock = TypeMoq.Mock.ofType<azdata.IConnectionProfile>();
iconnectionProfileMock.setup((x: any) => x.then).returns(() => undefined);
const close = sinon.stub(ConnectToPGSqlDialog.prototype, 'waitForClose').returns(Promise.resolve(iconnectionProfileMock.object));
const cancelButtonMock = TypeMoq.Mock.ofType<azdata.window.Button>();
cancelButtonMock.setup((x: any) => x.then).returns(() => undefined);
const dialogMock = TypeMoq.Mock.ofType<azdata.window.Dialog>();
dialogMock.setup(x => x.cancelButton).returns(() => cancelButtonMock.object);
dialogMock.setup((x: any) => x.then).returns(() => undefined);
const show = sinon.stub(azdata.window, 'createModelViewDialog').returns(dialogMock.object);
sinon.stub(azdata.window, 'openDialog');
await postgresModel['getConnectionProfile']();
sinon.assert.calledOnce(connect);
sinon.assert.calledOnce(show);
sinon.assert.calledOnce(close);
});
it('Show dialog prompt if username not found', async function (): Promise<void> {
// Setup PostgresModel without username
const postgresResource: PGResourceInfo = { name: 'pgt', resourceType: '', connectionId: '12345678' };
const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances };
let postgresModelNew = new PostgresModel(controllerModel, postgresResource, registration, new AzureArcTreeDataProvider(TypeMoq.Mock.ofType<vscode.ExtensionContext>().object));
await postgresModelNew.refresh();
const password = generateGuid();
// Set up cred store to return our password
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: password }));
credProviderMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.credentials, 'getProvider').returns(Promise.resolve(credProviderMock.object));
const connect = sinon.stub(azdata.connection, 'connect');
const cancelButtonMock = TypeMoq.Mock.ofType<azdata.window.Button>();
cancelButtonMock.setup((x: any) => x.then).returns(() => undefined);
const dialogMock = TypeMoq.Mock.ofType<azdata.window.Dialog>();
dialogMock.setup(x => x.cancelButton).returns(() => cancelButtonMock.object);
dialogMock.setup((x: any) => x.then).returns(() => undefined);
const show = sinon.stub(azdata.window, 'createModelViewDialog').returns(dialogMock.object);
sinon.stub(azdata.window, 'openDialog');
const iconnectionProfileMock = TypeMoq.Mock.ofType<azdata.IConnectionProfile>();
iconnectionProfileMock.setup((x: any) => x.then).returns(() => undefined);
const close = sinon.stub(ConnectToPGSqlDialog.prototype, 'waitForClose').returns(Promise.resolve(iconnectionProfileMock.object));
await postgresModelNew['getConnectionProfile']();
sinon.assert.notCalled(connect);
sinon.assert.calledOnce(show);
sinon.assert.calledOnce(close);
});
it('Shows dialog prompt if no connection id', async function (): Promise<void> {
// Setup PostgresModel without connectionId
const postgresResource: PGResourceInfo = { name: 'pgt', resourceType: '' };
const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances };
let postgresModelNew = new PostgresModel(controllerModel, postgresResource, registration, new AzureArcTreeDataProvider(TypeMoq.Mock.ofType<vscode.ExtensionContext>().object));
await postgresModelNew.refresh();
const provider = sinon.stub(azdata.credentials, 'getProvider');
const connect = sinon.stub(azdata.connection, 'connect');
const cancelButtonMock = TypeMoq.Mock.ofType<azdata.window.Button>();
cancelButtonMock.setup((x: any) => x.then).returns(() => undefined);
const dialogMock = TypeMoq.Mock.ofType<azdata.window.Dialog>();
dialogMock.setup(x => x.cancelButton).returns(() => cancelButtonMock.object);
dialogMock.setup((x: any) => x.then).returns(() => undefined);
const show = sinon.stub(azdata.window, 'createModelViewDialog').returns(dialogMock.object);
sinon.stub(azdata.window, 'openDialog');
const iconnectionProfileMock = TypeMoq.Mock.ofType<azdata.IConnectionProfile>();
iconnectionProfileMock.setup((x: any) => x.then).returns(() => undefined);
const close = sinon.stub(ConnectToPGSqlDialog.prototype, 'waitForClose').returns(Promise.resolve(iconnectionProfileMock.object));
await postgresModelNew['getConnectionProfile']();
sinon.assert.notCalled(provider);
sinon.assert.notCalled(connect);
sinon.assert.calledOnce(show);
sinon.assert.calledOnce(close);
});
});
describe('getEngineSettings', function (): void {
beforeEach(async () => {
// Setup PostgresModel
const postgresResource: PGResourceInfo = { name: 'pgt', resourceType: '', userName: 'postgres', connectionId: '12345678' };
const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances };
postgresModel = new PostgresModel(controllerModel, postgresResource, registration, new AzureArcTreeDataProvider(TypeMoq.Mock.ofType<vscode.ExtensionContext>().object));
//Stub calling refresh postgres model
sinon.stub(azdataApi.arc.postgres.server, 'show').resolves(FakePostgresServerShowOutput);
//Stub how to get connection profile
const iconnectionProfileMock = TypeMoq.Mock.ofType<azdata.IConnectionProfile>();
iconnectionProfileMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(ConnectToPGSqlDialog.prototype, 'waitForClose').returns(Promise.resolve(iconnectionProfileMock.object));
const cancelButtonMock = TypeMoq.Mock.ofType<azdata.window.Button>();
cancelButtonMock.setup((x: any) => x.then).returns(() => undefined);
const dialogMock = TypeMoq.Mock.ofType<azdata.window.Dialog>();
dialogMock.setup(x => x.cancelButton).returns(() => cancelButtonMock.object);
dialogMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.window, 'createModelViewDialog').returns(dialogMock.object);
sinon.stub(azdata.window, 'openDialog');
sinon.stub(azdata.connection, 'getUriForConnection');
//Call to provide external endpoint
await postgresModel.refresh();
});
it('Throw error when trying to connect fails', async function (): Promise<void> {
const errorMessage = 'Mock connection fail occured';
const connectionResultMock = TypeMoq.Mock.ofType<azdata.ConnectionResult>();
connectionResultMock.setup(x => x.connected).returns(() => false);
connectionResultMock.setup(x => x.errorMessage).returns(() => errorMessage);
connectionResultMock.setup((x: any) => x.then).returns(() => undefined);
const connect = sinon.stub(azdata.connection, 'connect').returns(Promise.resolve(connectionResultMock.object));
await should(postgresModel.getEngineSettings()).be.rejectedWith(new Error(errorMessage));
sinon.assert.calledOnce(connect);
});
it('Update active connection id when connect passes', async function (): Promise<void> {
const connectionID = '098765';
const connectionResultMock = TypeMoq.Mock.ofType<azdata.ConnectionResult>();
connectionResultMock.setup(x => x.connected).returns(() => true);
connectionResultMock.setup(x => x.connectionId).returns(() => connectionID);
connectionResultMock.setup((x: any) => x.then).returns(() => undefined);
const connect = sinon.stub(azdata.connection, 'connect').returns(Promise.resolve(connectionResultMock.object));
const array: azdata.DbCellValue[][] = [];
const executeMock = TypeMoq.Mock.ofType<azdata.SimpleExecuteResult>();
executeMock.setup(x => x.rows).returns(() => array);
executeMock.setup((x: any) => x.then).returns(() => undefined);
const providerMock = TypeMoq.Mock.ofType<azdata.QueryProvider>();
providerMock.setup(x => x.runQueryAndReturn(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(async () => executeMock.object);
providerMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.dataprotocol, 'getProvider').returns(providerMock.object);
await postgresModel.getEngineSettings();
sinon.assert.calledOnce(connect);
sinon.assert.match(postgresModel['_activeConnectionId'], connectionID);
});
it('Updates engineSettingsLastUpdated after populating engine settings', async function (): Promise<void> {
const connectionResultMock = TypeMoq.Mock.ofType<azdata.ConnectionResult>();
connectionResultMock.setup(x => x.connected).returns(() => true);
connectionResultMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.connection, 'connect').returns(Promise.resolve(connectionResultMock.object));
const array: azdata.DbCellValue[][] = [];
const executeMock = TypeMoq.Mock.ofType<azdata.SimpleExecuteResult>();
executeMock.setup(x => x.rows).returns(() => array);
executeMock.setup((x: any) => x.then).returns(() => undefined);
const providerMock = TypeMoq.Mock.ofType<azdata.QueryProvider>();
providerMock.setup(x => x.runQueryAndReturn(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(async () => executeMock.object);
providerMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.dataprotocol, 'getProvider').returns(providerMock.object);
await postgresModel.getEngineSettings();
should(postgresModel.engineSettingsLastUpdated).be.Date();
});
it('Calls onEngineSettingsUpdated event after populating engine settings', async function (): Promise<void> {
const connectionResultMock = TypeMoq.Mock.ofType<azdata.ConnectionResult>();
connectionResultMock.setup(x => x.connected).returns(() => true);
connectionResultMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.connection, 'connect').returns(Promise.resolve(connectionResultMock.object));
const array: azdata.DbCellValue[][] = [];
const executeMock = TypeMoq.Mock.ofType<azdata.SimpleExecuteResult>();
executeMock.setup(x => x.rows).returns(() => array);
executeMock.setup((x: any) => x.then).returns(() => undefined);
const providerMock = TypeMoq.Mock.ofType<azdata.QueryProvider>();
providerMock.setup(x => x.runQueryAndReturn(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(async () => executeMock.object);
providerMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.dataprotocol, 'getProvider').returns(providerMock.object);
const onEngineSettingsUpdated = sinon.spy(vscode.EventEmitter.prototype, 'fire');
await postgresModel.getEngineSettings();
sinon.assert.calledOnceWithExactly(onEngineSettingsUpdated, postgresModel.workerNodesEngineSettings);
});
it('Populating engine settings skips certain parameters', async function (): Promise<void> {
const connectionResultMock = TypeMoq.Mock.ofType<azdata.ConnectionResult>();
connectionResultMock.setup(x => x.connected).returns(() => true);
connectionResultMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.connection, 'connect').returns(Promise.resolve(connectionResultMock.object));
const rows: azdata.DbCellValue[][] = [
[{
displayValue: 'archive_timeout',
isNull: false,
invariantCultureDisplayValue: ''
}]
];
const executeMock = TypeMoq.Mock.ofType<azdata.SimpleExecuteResult>();
executeMock.setup(x => x.rows).returns(() => rows);
executeMock.setup((x: any) => x.then).returns(() => undefined);
const providerMock = TypeMoq.Mock.ofType<azdata.QueryProvider>();
providerMock.setup(x => x.runQueryAndReturn(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(async () => executeMock.object);
providerMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.dataprotocol, 'getProvider').returns(providerMock.object);
await postgresModel.getEngineSettings();
should(postgresModel.workerNodesEngineSettings.pop()).be.undefined();
});
it('Populates engine settings accurately', async function (): Promise<void> {
const connectionResultMock = TypeMoq.Mock.ofType<azdata.ConnectionResult>();
connectionResultMock.setup(x => x.connected).returns(() => true);
connectionResultMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.connection, 'connect').returns(Promise.resolve(connectionResultMock.object));
const rows: azdata.DbCellValue[][] = [
[{
displayValue: 'test0',
isNull: false,
invariantCultureDisplayValue: ''
},
{
displayValue: 'test1',
isNull: false,
invariantCultureDisplayValue: ''
},
{
displayValue: 'test2',
isNull: false,
invariantCultureDisplayValue: ''
},
{
displayValue: 'test3',
isNull: false,
invariantCultureDisplayValue: ''
},
{
displayValue: 'test4',
isNull: false,
invariantCultureDisplayValue: ''
},
{
displayValue: 'test5',
isNull: false,
invariantCultureDisplayValue: ''
},
{
displayValue: 'test6',
isNull: false,
invariantCultureDisplayValue: ''
}],
];
const engineSettingsModelCompare: EngineSettingsModel = {
parameterName: 'test0',
value: 'test1',
description: 'test2',
min: 'test3',
max: 'test4',
options: 'test5',
type: 'test6'
};
const executeMock = TypeMoq.Mock.ofType<azdata.SimpleExecuteResult>();
executeMock.setup(x => x.rows).returns(() => rows);
executeMock.setup((x: any) => x.then).returns(() => undefined);
const providerMock = TypeMoq.Mock.ofType<azdata.QueryProvider>();
providerMock.setup(x => x.runQueryAndReturn(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(async () => executeMock.object);
providerMock.setup((x: any) => x.then).returns(() => undefined);
sinon.stub(azdata.dataprotocol, 'getProvider').returns(providerMock.object);
await postgresModel.getEngineSettings();
should(postgresModel.workerNodesEngineSettings.pop()).be.match(engineSettingsModelCompare);
});
});
});

View File

@@ -0,0 +1,163 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { PGResourceInfo, ResourceType } from 'arc';
import * as azdataExt from 'azdata-ext';
import * as should from 'should';
import * as sinon from 'sinon';
import * as TypeMoq from 'typemoq';
import * as vscode from 'vscode';
import { createModelViewMock } from '@microsoft/azdata-test/out/mocks/modelView/modelViewMock';
import { ControllerModel, Registration } from '../../../models/controllerModel';
import { PostgresModel } from '../../../models/postgresModel';
import { PostgresConnectionStringsPage } from '../../../ui/dashboards/postgres/postgresConnectionStringsPage';
import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider';
import { FakeControllerModel } from '../../mocks/fakeControllerModel';
import { FakeAzdataApi } from '../../mocks/fakeAzdataApi';
export const FakePostgresServerShowOutput: azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult> = {
logs: [],
stdout: [],
stderr: [],
result: {
apiVersion: 'version',
kind: 'postgresql',
metadata: {
creationTimestamp: '',
generation: 1,
name: 'pgt',
namespace: 'ns',
resourceVersion: '',
selfLink: '',
uid: '',
},
spec: {
engine: {
extensions: [{ name: '' }],
settings: {
default: { ['']: '' }
},
version: '12'
},
scale: {
shards: 0,
workers: 0
},
scheduling: {
default: {
resources: {
requests: {
cpu: '',
memory: ''
},
limits: {
cpu: '',
memory: ''
}
}
}
},
service: {
type: '',
port: 0
},
storage: {
data: {
className: '',
size: ''
},
logs: {
className: '',
size: ''
},
backups: {
className: '',
size: ''
}
}
},
status: {
externalEndpoint: '127.0.0.1:5432',
readyPods: '',
state: '',
logSearchDashboard: '',
metricsDashboard: '',
podsStatus: [{
conditions: [{
lastTransitionTime: '',
message: '',
reason: '',
status: '',
type: '',
}],
name: '',
role: '',
}]
}
}
};
describe('postgresConnectionStringsPage', function (): void {
let controllerModel: ControllerModel;
let postgresModel: PostgresModel;
let azdataApi: azdataExt.IAzdataApi;
let postgresConnectionStrings: PostgresConnectionStringsPage;
afterEach(function (): void {
sinon.restore();
});
beforeEach(async () => {
// Stub the azdata CLI API
azdataApi = new FakeAzdataApi();
const azdataExt = TypeMoq.Mock.ofType<azdataExt.IExtension>();
azdataExt.setup(x => x.azdata).returns(() => azdataApi);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExt.object });
// Setup Controller Model
controllerModel = new FakeControllerModel();
//Stub calling azdata login and acquiring session
sinon.stub(controllerModel, 'login').returns(Promise.resolve());
// Setup PostgresModel
const postgresResource: PGResourceInfo = { name: 'pgt', resourceType: '' };
const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances };
postgresModel = new PostgresModel(controllerModel, postgresResource, registration, new AzureArcTreeDataProvider(TypeMoq.Mock.ofType<vscode.ExtensionContext>().object));
// Setup stub of show call
const postgresShow = sinon.stub().returns(FakePostgresServerShowOutput);
sinon.stub(azdataApi, 'arc').get(() => {
return { postgres: { server: { show(name: string) { return postgresShow(name); } } } };
});
// Setup the PostgresConnectionsStringsPage
let { modelViewMock } = createModelViewMock();
postgresConnectionStrings = new PostgresConnectionStringsPage(modelViewMock.object, undefined!, postgresModel);
});
describe('getConnectionStrings', function (): void {
it('Strings container should be empty since postgres model has not been refreshed', async function (): Promise<void> {
should(postgresConnectionStrings['getConnectionStrings']()).be.empty();
});
it('String contain correct ip and port', async function (): Promise<void> {
// Call to provide external endpoint
await postgresModel.refresh();
let endpoint = FakePostgresServerShowOutput.result.status.externalEndpoint.split(':');
postgresConnectionStrings['getConnectionStrings']().forEach(k => {
should(k.value.includes(endpoint[0])).be.True();
should(k.value.includes(endpoint[1])).be.True();
});
});
});
});

View File

@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as sinon from 'sinon';
import * as TypeMoq from 'typemoq';
import * as azdataExt from 'azdata-ext';
import * as utils from '../../../common/utils';
import * as loc from '../../../localizedConstants';
import { Deferred } from '../../../common/promise';
import { createModelViewMock } from '@microsoft/azdata-test/out/mocks/modelView/modelViewMock';
import { StubButton } from '@microsoft/azdata-test/out/stubs/modelView/stubButton';
import { PGResourceInfo, ResourceType } from 'arc';
import { PostgresOverviewPage } from '../../../ui/dashboards/postgres/postgresOverviewPage';
import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider';
import { FakeControllerModel } from '../../mocks/fakeControllerModel';
import { FakeAzdataApi } from '../../mocks/fakeAzdataApi';
import { PostgresModel } from '../../../models/postgresModel';
import { ControllerModel, Registration } from '../../../models/controllerModel';
describe('postgresOverviewPage', () => {
let postgresOverview: PostgresOverviewPage;
let azdataApi: azdataExt.IAzdataApi;
let controllerModel: ControllerModel;
let postgresModel: PostgresModel;
let showInformationMessage: sinon.SinonStub;
let showErrorMessage: sinon.SinonStub;
let informationMessageShown: Deferred;
let errorMessageShown: Deferred;
beforeEach(async () => {
// Stub the azdata CLI API
azdataApi = new FakeAzdataApi();
const azdataExt = TypeMoq.Mock.ofType<azdataExt.IExtension>();
azdataExt.setup(x => x.azdata).returns(() => azdataApi);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExt.object });
// Stub the window UI
informationMessageShown = new Deferred();
showInformationMessage = sinon.stub(vscode.window, 'showInformationMessage').callsFake(
(_: string, __: vscode.MessageOptions, ...___: vscode.MessageItem[]) => {
informationMessageShown.resolve();
return Promise.resolve(undefined);
});
errorMessageShown = new Deferred();
showErrorMessage = sinon.stub(vscode.window, 'showErrorMessage').callsFake(
(_: string, __: vscode.MessageOptions, ...___: vscode.MessageItem[]) => {
errorMessageShown.resolve();
return Promise.resolve(undefined);
});
// Setup the PostgresModel
controllerModel = new FakeControllerModel();
const postgresResource: PGResourceInfo = { name: 'my-pg', resourceType: '' };
const registration: Registration = { instanceName: '', state: '', instanceType: ResourceType.postgresInstances };
const treeDataProvider = new AzureArcTreeDataProvider(TypeMoq.Mock.ofType<vscode.ExtensionContext>().object);
postgresModel = new PostgresModel(controllerModel, postgresResource, registration, treeDataProvider);
// Setup the PostgresOverviewPage
const { modelViewMock } = createModelViewMock();
postgresOverview = new PostgresOverviewPage(modelViewMock.object, undefined!, controllerModel, postgresModel);
// Call the getter to initialize toolbar, but we don't need to use it for anything
// eslint-disable-next-line code-no-unused-expressions
postgresOverview['toolbarContainer'];
});
afterEach(() => {
sinon.restore();
});
describe('delete button', () => {
let refreshTreeNode: sinon.SinonStub;
beforeEach(() => {
sinon.stub(utils, 'promptForInstanceDeletion').returns(Promise.resolve(true));
sinon.stub(controllerModel, 'login').returns(Promise.resolve());
refreshTreeNode = sinon.stub(controllerModel, 'refreshTreeNode');
});
it('deletes Postgres on success', async () => {
// Stub 'azdata arc postgres server delete' to return success
const postgresDeleteStub = sinon.stub(azdataApi.arc.postgres.server, 'delete');
(postgresOverview['deleteButton'] as StubButton).click();
await informationMessageShown;
sinon.assert.calledOnceWithExactly(postgresDeleteStub, postgresModel.info.name, sinon.match.any, sinon.match.any);
sinon.assert.calledOnceWithExactly(showInformationMessage, loc.instanceDeleted(postgresModel.info.name));
sinon.assert.notCalled(showErrorMessage);
sinon.assert.calledOnce(refreshTreeNode);
});
it('shows an error message on failure', async () => {
// Stub 'azdata arc postgres server delete' to throw an exception
const error = new Error('something bad happened');
const postgresDeleteStub = sinon.stub(azdataApi.arc.postgres.server, 'delete').throws(error);
(postgresOverview['deleteButton'] as StubButton).click();
await errorMessageShown;
sinon.assert.calledOnceWithExactly(postgresDeleteStub, postgresModel.info.name, sinon.match.any, sinon.match.any);
sinon.assert.notCalled(showInformationMessage);
sinon.assert.calledOnceWithExactly(showErrorMessage, loc.instanceDeletionFailed(postgresModel.info.name, error.message));
sinon.assert.notCalled(refreshTreeNode);
});
});
});

View File

@@ -18,8 +18,8 @@ describe('ConnectControllerDialog', function (): void {
(<{ info: ControllerInfo | undefined, description: string }[]>[
{ info: undefined, description: 'all input' },
{ info: { url: '127.0.0.1' }, description: 'all but URL' },
{ info: { url: '127.0.0.1', username: 'sa' }, description: 'all but URL and password' }]).forEach(test => {
{ info: { endpoint: '127.0.0.1' }, description: 'all but URL' },
{ info: { endpoint: '127.0.0.1', username: 'sa' }, description: 'all but URL and password' }]).forEach(test => {
it(`Validate returns false when ${test.description} is empty`, async function (): Promise<void> {
const connectControllerDialog = new ConnectToControllerDialog(undefined!);
connectControllerDialog.showDialog(test.info, undefined);
@@ -32,7 +32,7 @@ describe('ConnectControllerDialog', function (): void {
it('validate returns false if controller refresh fails', async function (): Promise<void> {
sinon.stub(ControllerModel.prototype, 'refresh').returns(Promise.reject('Controller refresh failed'));
const connectControllerDialog = new ConnectToControllerDialog(undefined!);
const info = { id: uuid(), url: 'https://127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] };
const info: ControllerInfo = { id: uuid(), endpoint: 'https://127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] };
connectControllerDialog.showDialog(info, 'pwd');
await connectControllerDialog.isInitialized;
const validateResult = await connectControllerDialog.validate();
@@ -41,36 +41,36 @@ describe('ConnectControllerDialog', function (): void {
it('validate replaces http with https', async function (): Promise<void> {
await validateConnectControllerDialog(
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
{ id: uuid(), endpoint: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30081');
});
it('validate appends https if missing', async function (): Promise<void> {
await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
await validateConnectControllerDialog({ id: uuid(), endpoint: '127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30080');
});
it('validate appends default port if missing', async function (): Promise<void> {
await validateConnectControllerDialog({ id: uuid(), url: 'https://127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
await validateConnectControllerDialog({ id: uuid(), endpoint: 'https://127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30080');
});
it('validate appends both port and https if missing', async function (): Promise<void> {
await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
await validateConnectControllerDialog({ id: uuid(), endpoint: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30080');
});
for (const name of ['', undefined]) {
it.skip(`validate display name gets set to arc instance name for user chosen name of:${name}`, async function (): Promise<void> {
await validateConnectControllerDialog(
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: name!, username: 'sa', rememberPassword: true, resources: [] },
{ id: uuid(), endpoint: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: name!, namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30081');
});
}
it.skip(`validate display name gets set to default data controller name for user chosen name of:'' and instanceName in explicably returned as undefined from the controller endpoint`, async function (): Promise<void> {
await validateConnectControllerDialog(
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: '', username: 'sa', rememberPassword: true, resources: [] },
{ id: uuid(), endpoint: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: '', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30081',
undefined);
});
@@ -92,6 +92,6 @@ async function validateConnectControllerDialog(info: ControllerInfo, expectedUrl
const validateResult = await connectControllerDialog.validate();
should(validateResult).be.true('Validation should have returned true');
const model = await connectControllerDialog.waitForClose();
should(model?.controllerModel.info.url).equal(expectedUrl);
should(model?.controllerModel.info.endpoint).equal(expectedUrl);
should(model?.controllerModel.info.name).equal(expectedControllerInfoName);
}

View File

@@ -24,6 +24,20 @@ interface ExtensionGlobalMemento extends vscode.Memento {
setKeysForSync(keys: string[]): void;
}
function getDefaultControllerInfo(): ControllerInfo {
return {
id: uuid(),
endpoint: '127.0.0.1',
kubeConfigFilePath: '/path/to/.kube/config',
kubeClusterContext: 'currentCluster',
username: 'sa',
name: 'my-arc',
namespace: 'arc-ns',
rememberPassword: true,
resources: []
};
}
describe('AzureArcTreeDataProvider tests', function (): void {
let treeDataProvider: AzureArcTreeDataProvider;
beforeEach(function (): void {
@@ -58,7 +72,7 @@ describe('AzureArcTreeDataProvider tests', function (): void {
treeDataProvider['_loading'] = false;
let children = await treeDataProvider.getChildren();
should(children.length).equal(0, 'There initially shouldn\'t be any children');
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
await treeDataProvider.addOrUpdateController(controllerModel, '');
should(children.length).equal(1, 'Controller node should be added correctly');
await treeDataProvider.addOrUpdateController(controllerModel, '');
@@ -69,12 +83,12 @@ describe('AzureArcTreeDataProvider tests', function (): void {
treeDataProvider['_loading'] = false;
let children = await treeDataProvider.getChildren();
should(children.length).equal(0, 'There initially shouldn\'t be any children');
const originalInfo: ControllerInfo = { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] };
const originalInfo: ControllerInfo = getDefaultControllerInfo();
const controllerModel = new ControllerModel(treeDataProvider, originalInfo);
await treeDataProvider.addOrUpdateController(controllerModel, '');
should(children.length).equal(1, 'Controller node should be added correctly');
should((<ControllerTreeNode>children[0]).model.info).deepEqual(originalInfo);
const newInfo = { id: originalInfo.id, url: '1.1.1.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'new-name', username: 'admin', rememberPassword: false, resources: [] };
const newInfo: ControllerInfo = { id: originalInfo.id, endpoint: '1.1.1.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'new-name', namespace: 'new-namespace', username: 'admin', rememberPassword: false, resources: [] };
const controllerModel2 = new ControllerModel(treeDataProvider, newInfo);
await treeDataProvider.addOrUpdateController(controllerModel2, '');
should(children.length).equal(1, 'Shouldn\'t add duplicate controller node');
@@ -102,18 +116,20 @@ describe('AzureArcTreeDataProvider tests', function (): void {
return mockArcApi.object;
});
const fakeAzdataApi = new FakeAzdataApi();
fakeAzdataApi.postgresInstances = [{ name: 'pg1', state: '', workers: 0 }];
fakeAzdataApi.miaaInstances = [{ name: 'miaa1', state: '', replicas: '', serverEndpoint: '' }];
const pgInstances = [{ name: 'pg1', state: '', workers: 0 }];
const miaaInstances = [{ name: 'miaa1', state: '', replicas: '', serverEndpoint: '' }];
fakeAzdataApi.postgresInstances = pgInstances;
fakeAzdataApi.miaaInstances = miaaInstances;
mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi);
sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object);
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').resolves([{ name: 'currentCluster', isCurrentContext: true }]);
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, 'mypassword');
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').returns([{ name: 'currentCluster', isCurrentContext: true }]);
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo(), 'mypassword');
await treeDataProvider.addOrUpdateController(controllerModel, '');
const controllerNode = treeDataProvider.getControllerNode(controllerModel);
const children = await treeDataProvider.getChildren(controllerNode);
should(children.filter(c => c.label === fakeAzdataApi.postgresInstances[0].name).length).equal(1, 'Should have a Postgres child');
should(children.filter(c => c.label === fakeAzdataApi.miaaInstances[0].name).length).equal(1, 'Should have a MIAA child');
should(children.filter(c => c.label === pgInstances[0].name).length).equal(1, 'Should have a Postgres child');
should(children.filter(c => c.label === miaaInstances[0].name).length).equal(1, 'Should have a MIAA child');
should(children.length).equal(2, 'Should have exactly 2 children');
});
});
@@ -121,8 +137,10 @@ describe('AzureArcTreeDataProvider tests', function (): void {
describe('removeController', function (): void {
it('removing a controller should work as expected', async function (): Promise<void> {
treeDataProvider['_loading'] = false;
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
const controllerModel2 = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.2', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'cloudsa', rememberPassword: true, resources: [] });
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
const info2 = getDefaultControllerInfo();
info2.username = 'cloudsa';
const controllerModel2 = new ControllerModel(treeDataProvider, info2);
await treeDataProvider.addOrUpdateController(controllerModel, '');
await treeDataProvider.addOrUpdateController(controllerModel2, '');
const children = <ControllerTreeNode[]>(await treeDataProvider.getChildren());
@@ -139,20 +157,20 @@ describe('AzureArcTreeDataProvider tests', function (): void {
describe('openResourceDashboard', function (): void {
it('Opening dashboard for nonexistent controller node throws', async function (): Promise<void> {
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
await should(openDashboardPromise).be.rejected();
});
it('Opening dashboard for nonexistent resource throws', async function (): Promise<void> {
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
await treeDataProvider.addOrUpdateController(controllerModel, '');
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
await should(openDashboardPromise).be.rejected();
});
it('Opening dashboard for existing resource node succeeds', async function (): Promise<void> {
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
const miaaModel = new MiaaModel(controllerModel, { name: 'miaa-1', resourceType: ResourceType.sqlManagedInstances }, undefined!, treeDataProvider);
await treeDataProvider.addOrUpdateController(controllerModel, '');
const controllerNode = treeDataProvider.getControllerNode(controllerModel)!;

View File

@@ -37,7 +37,8 @@ declare module 'arc' {
id: string,
kubeConfigFilePath: string,
kubeClusterContext: string
url: string,
endpoint: string | undefined,
namespace: string,
name: string,
username: string,
rememberPassword: boolean,

View File

@@ -7,7 +7,7 @@ import * as azdata from 'azdata';
export abstract class Dashboard {
private dashboard!: azdata.window.ModelViewDashboard;
protected dashboard!: azdata.window.ModelViewDashboard;
constructor(protected title: string, protected readonly name: string) { }
@@ -16,6 +16,10 @@ export abstract class Dashboard {
await this.dashboard.open();
}
public async closeDashboard(): Promise<void> {
await this.dashboard.close();
}
protected createDashboard(): azdata.window.ModelViewDashboard {
const dashboard = azdata.window.createModelViewDashboard(this.title, this.name);
dashboard.registerTabs(async modelView => {

View File

@@ -11,7 +11,7 @@ export abstract class DashboardPage extends InitializingComponent {
protected disposables: vscode.Disposable[] = [];
constructor(protected modelView: azdata.ModelView) {
constructor(protected modelView: azdata.ModelView, protected dashboard: azdata.window.ModelViewDashboard) {
super();
this.disposables.push(modelView.onClosed(() => {
// Clean up best we can

View File

@@ -17,6 +17,9 @@ export class RadioOptionsGroup {
private _loadingBuilder: azdata.LoadingComponentBuilder;
private _currentRadioOption!: azdata.RadioButtonComponent;
private _onRadioOptionChanged: vscode.EventEmitter<string | undefined> = new vscode.EventEmitter<string | undefined>();
public onRadioOptionChanged: vscode.Event<string | undefined> = this._onRadioOptionChanged.event;
constructor(private _modelBuilder: azdata.ModelBuilder, private _onNewDisposableCreated: (disposable: vscode.Disposable) => void, private _groupName: string = `RadioOptionsGroup${RadioOptionsGroup.id++}`) {
this._divContainer = this._modelBuilder.divContainer().withProperties<azdata.DivContainerProperties>({ clickable: false }).component();
this._loadingBuilder = this._modelBuilder.loadingComponent().withItem(this._divContainer);
@@ -26,7 +29,7 @@ export class RadioOptionsGroup {
return this._loadingBuilder.component();
}
async load(optionsInfoGetter: () => Promise<RadioOptionsInfo>): Promise<void> {
async load(optionsInfoGetter: () => RadioOptionsInfo | Promise<RadioOptionsInfo>): Promise<void> {
this.component().loading = true;
this._divContainer.clearItems();
try {
@@ -51,6 +54,7 @@ export class RadioOptionsGroup {
// it is just better to keep things clean.
this._currentRadioOption.checked = false;
this._currentRadioOption = radioOption;
this._onRadioOptionChanged.fire(this.value);
}
}));
this._divContainer.addItem(radioOption);

View File

@@ -22,7 +22,7 @@ export class ControllerDashboard extends Dashboard {
}
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
const overviewPage = new ControllerDashboardOverviewPage(modelView, this._controllerModel);
const overviewPage = new ControllerDashboardOverviewPage(modelView, this.dashboard, this._controllerModel);
return [
overviewPage.tab
];

View File

@@ -35,8 +35,8 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
instanceNamespace: '-',
};
constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel) {
super(modelView);
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel) {
super(modelView, dashboard);
this._azurecoreApi = vscode.extensions.getExtension(azurecore.extension.name)?.exports;
@@ -147,7 +147,12 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
this.disposables.push(
newInstance.onDidClick(async () => {
await vscode.commands.executeCommand('azdata.resource.deploy', 'azure-sql-mi', ['azure-sql-mi', 'arc.postgres'], { 'azure-sql-mi': { 'mi-type': ['arc-mi'] } });
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
{ 'CONTROLLER_NAME': node?.label });
}));
// Refresh

View File

@@ -32,8 +32,8 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
private readonly _azdataApi: azdataExt.IExtension;
constructor(protected modelView: azdata.ModelView, private _miaaModel: MiaaModel) {
super(modelView);
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _miaaModel: MiaaModel) {
super(modelView, dashboard);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this.initializeConfigurationBoxes();
@@ -129,19 +129,18 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
cancellable: false
},
async (_progress, _token): Promise<void> => {
let session: azdataExt.AzdataSession | undefined = undefined;
try {
session = await this._miaaModel.controllerModel.acquireAzdataSession();
await this._azdataApi.azdata.arc.sql.mi.edit(
this._miaaModel.info.name, this.saveArgs, this._miaaModel.controllerModel.azdataAdditionalEnvVars, session);
this._miaaModel.info.name, this.saveArgs, this._miaaModel.controllerModel.azdataAdditionalEnvVars, this._miaaModel.controllerModel.controllerContext);
} catch (err) {
this.saveButton!.enabled = true;
throw err;
} finally {
session?.dispose();
}
await this._miaaModel.refresh();
try {
await this._miaaModel.refresh();
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
}
}
);
@@ -217,7 +216,6 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
this.memoryLimitBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 2,
validationErrorMessage: loc.memoryLimitValidationErrorMessage,
inputType: 'number',
placeHolder: loc.loading
}).component();
@@ -235,7 +233,6 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
this.memoryRequestBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 2,
validationErrorMessage: loc.memoryRequestValidationErrorMessage,
inputType: 'number',
placeHolder: loc.loading
}).component();
@@ -320,7 +317,6 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
currentCPUSize = '';
}
this.coresRequestBox!.validationErrorMessage = loc.validationMin(this.coresRequestBox!.min!);
this.coresRequestBox!.placeHolder = currentCPUSize;
this.coresRequestBox!.value = '';
this.saveArgs.coresRequest = undefined;
@@ -331,7 +327,6 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
currentCPUSize = '';
}
this.coresLimitBox!.validationErrorMessage = loc.validationMin(this.coresLimitBox!.min!);
this.coresLimitBox!.placeHolder = currentCPUSize;
this.coresLimitBox!.value = '';
this.saveArgs.coresLimit = undefined;

View File

@@ -8,7 +8,6 @@ import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants';
import { KeyValueContainer, KeyValue, InputKeyValue, MultilineInputKeyValue } from '../../components/keyValueContainer';
import { DashboardPage } from '../../components/dashboardPage';
import { ControllerModel } from '../../../models/controllerModel';
import { MiaaModel } from '../../../models/miaaModel';
import { parseIpAndPort } from '../../../common/utils';
@@ -17,9 +16,9 @@ export class MiaaConnectionStringsPage extends DashboardPage {
private _keyValueContainer!: KeyValueContainer;
private _connectionStringsMessage!: azdata.TextComponent;
constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
super(modelView);
this.disposables.push(this._controllerModel.onRegistrationsUpdated(_ =>
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _miaaModel: MiaaModel) {
super(modelView, dashboard);
this.disposables.push(this._miaaModel.onConfigUpdated(_ =>
this.eventuallyRunOnInitialized(() => this.updateConnectionStrings())));
}

View File

@@ -26,9 +26,9 @@ export class MiaaDashboard extends Dashboard {
}
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
const overviewPage = new MiaaDashboardOverviewPage(modelView, this._controllerModel, this._miaaModel);
const connectionStringsPage = new MiaaConnectionStringsPage(modelView, this._controllerModel, this._miaaModel);
const computeAndStoragePage = new MiaaComputeAndStoragePage(modelView, this._miaaModel);
const overviewPage = new MiaaDashboardOverviewPage(modelView, this.dashboard, this._controllerModel, this._miaaModel);
const connectionStringsPage = new MiaaConnectionStringsPage(modelView, this.dashboard, this._miaaModel);
const computeAndStoragePage = new MiaaComputeAndStoragePage(modelView, this.dashboard, this._miaaModel);
return [
overviewPage.tab,
{

View File

@@ -48,8 +48,8 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
vCores: ''
};
constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
super(modelView);
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
super(modelView, dashboard);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this._azurecoreApi = vscode.extensions.getExtension(azurecore.extension.name)?.exports;
@@ -244,17 +244,18 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
cancellable: false
},
async (_progress, _token) => {
const session = await this._controllerModel.acquireAzdataSession();
try {
return await this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name, this._controllerModel.azdataAdditionalEnvVars, session);
} finally {
session.dispose();
}
return await this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name, this._controllerModel.azdataAdditionalEnvVars, this._controllerModel.controllerContext);
}
);
await this._controllerModel.refreshTreeNode();
vscode.window.showInformationMessage(loc.instanceDeleted(this._miaaModel.info.name));
try {
await this.dashboard.close();
} catch (err) {
// Failures closing the dashboard aren't something we need to show users
console.log('Error closing MIAA dashboard ', err);
}
}
} catch (error) {
vscode.window.showErrorMessage(loc.instanceDeletionFailed(this._miaaModel.info.name, error));

View File

@@ -12,30 +12,43 @@ import { DashboardPage } from '../../components/dashboardPage';
import { PostgresModel } from '../../../models/postgresModel';
import { convertToGibibyteString } from '../../../common/utils';
export type ConfigurationSpecModel = {
workers?: number,
workerCoresRequest?: string,
workerCoresLimit?: string,
workerMemoryRequest?: string,
workerMemoryLimit?: string,
coordinatorCoresRequest?: string,
coordinatorCoresLimit?: string,
coordinatorMemoryRequest?: string,
coordinatorMemoryLimit?: string
};
export class PostgresComputeAndStoragePage extends DashboardPage {
private workerContainer?: azdata.DivContainer;
private workerContainer!: azdata.DivContainer;
private coordinatorContainer!: azdata.DivContainer;
private workerBox?: azdata.InputBoxComponent;
private coresLimitBox?: azdata.InputBoxComponent;
private coresRequestBox?: azdata.InputBoxComponent;
private memoryLimitBox?: azdata.InputBoxComponent;
private memoryRequestBox?: azdata.InputBoxComponent;
private workerBox!: azdata.InputBoxComponent;
private workerCoresLimitBox!: azdata.InputBoxComponent;
private workerCoresRequestBox!: azdata.InputBoxComponent;
private workerMemoryLimitBox!: azdata.InputBoxComponent;
private workerMemoryRequestBox!: azdata.InputBoxComponent;
private discardButton?: azdata.ButtonComponent;
private saveButton?: azdata.ButtonComponent;
private coordinatorCoresLimitBox!: azdata.InputBoxComponent;
private coordinatorCoresRequestBox!: azdata.InputBoxComponent;
private coordinatorMemoryLimitBox!: azdata.InputBoxComponent;
private coordinatorMemoryRequestBox!: azdata.InputBoxComponent;
private saveArgs: {
workers?: number,
coresLimit?: string,
coresRequest?: string,
memoryLimit?: string,
memoryRequest?: string
} = {};
private currentConfiguration: ConfigurationSpecModel = {};
private saveArgs: ConfigurationSpecModel = {};
private discardButton!: azdata.ButtonComponent;
private saveButton!: azdata.ButtonComponent;
private readonly _azdataApi: azdataExt.IExtension;
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
super(modelView);
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _postgresModel: PostgresModel) {
super(modelView, dashboard);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this.initializeConfigurationBoxes();
@@ -61,16 +74,16 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
const content = this.modelView.modelBuilder.divContainer().component();
root.addItem(content, { CSSStyles: { 'margin': '20px' } });
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
content.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.computeAndStorage,
CSSStyles: { ...cssStyles.title }
}).component());
const infoComputeStorage_p1 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
const infoComputeStorage_p1 = this.modelView.modelBuilder.text().withProps({
value: loc.postgresComputeAndStorageDescriptionPartOne,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'max-width': 'auto' }
}).component();
const infoComputeStorage_p2 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
const infoComputeStorage_p2 = this.modelView.modelBuilder.text().withProps({
value: loc.postgresComputeAndStorageDescriptionPartTwo,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
@@ -81,7 +94,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
const infoComputeStorage_p3 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
const infoComputeStorage_p3 = this.modelView.modelBuilder.text().withProps({
value: loc.computeAndStorageDescriptionPartThree,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
@@ -92,17 +105,17 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
const infoComputeStorage_p4 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
const infoComputeStorage_p4 = this.modelView.modelBuilder.text().withProps({
value: loc.computeAndStorageDescriptionPartFour,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
const infoComputeStorage_p5 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
const infoComputeStorage_p5 = this.modelView.modelBuilder.text().withProps({
value: loc.computeAndStorageDescriptionPartFive,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
const infoComputeStorage_p6 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
const infoComputeStorage_p6 = this.modelView.modelBuilder.text().withProps({
value: loc.computeAndStorageDescriptionPartSix,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
@@ -122,15 +135,26 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
.component();
content.addItem(computeInfoAndLinks, { CSSStyles: { 'min-height': '30px' } });
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
// Worker nodes section
this.workerContainer = this.modelView.modelBuilder.divContainer().component();
this.workerContainer.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.workerNodes,
CSSStyles: { ...cssStyles.title, 'margin-top': '25px' }
}).component());
this.workerContainer = this.modelView.modelBuilder.divContainer().component();
this.workerContainer.addItems(this.createUserInputSection(), { CSSStyles: { 'min-height': '30px' } });
this.workerContainer.addItems(this.createUserInputWorkerSection(), { CSSStyles: { 'min-height': '30px' } });
content.addItem(this.workerContainer, { CSSStyles: { 'min-height': '30px' } });
// Coordinator node section
this.coordinatorContainer = this.modelView.modelBuilder.divContainer().component();
this.coordinatorContainer.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.coordinatorNode,
CSSStyles: { ...cssStyles.title, 'margin-top': '25px' }
}).component());
this.coordinatorContainer.addItems(this.createUserInputCoordinatorSection(), { CSSStyles: { 'min-height': '30px' } });
// TODO unhide once once ready to make azdata calls
// content.addItem(this.coordinatorContainer, { CSSStyles: { 'min-height': '30px' } });
this.initialized = true;
return root;
@@ -138,7 +162,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
protected get toolbarContainer(): azdata.ToolbarContainer {
// Save Edits
this.saveButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
this.saveButton = this.modelView.modelBuilder.button().withProps({
label: loc.saveText,
iconPath: IconPathHelper.save,
enabled: false
@@ -146,7 +170,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.disposables.push(
this.saveButton.onDidClick(async () => {
this.saveButton!.enabled = false;
this.saveButton.enabled = false;
try {
await vscode.window.withProgress(
{
@@ -155,31 +179,47 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
cancellable: false
},
async (_progress, _token): Promise<void> => {
let session: azdataExt.AzdataSession | undefined = undefined;
try {
session = await this._postgresModel.controllerModel.acquireAzdataSession();
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
this.saveArgs,
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session
);
{
workers: this.saveArgs.workers,
coresRequest: this.saveArgs.workerCoresRequest,
coresLimit: this.saveArgs.workerCoresLimit,
memoryRequest: this.saveArgs.workerMemoryRequest,
memoryLimit: this.saveArgs.workerMemoryLimit
},
this._postgresModel.controllerModel.azdataAdditionalEnvVars);
/* TODO add second edit call for coordinator configuration
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{
coresRequest: this.saveArgs.coordinatorCoresRequest,
coresLimit: this.saveArgs.coordinatorCoresLimit,
memoryRequest: this.saveArgs.coordinatorMemoryRequest,
memoryLimit: this.saveArgs.coordinatorMemoryLimit
},
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session
);
*/
} catch (err) {
// If an error occurs while editing the instance then re-enable the save button since
// the edit wasn't successfully applied
this.saveButton!.enabled = true;
this.saveButton.enabled = true;
throw err;
} finally {
session?.dispose();
}
await this._postgresModel.refresh();
try {
await this._postgresModel.refresh();
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
}
}
);
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
this.discardButton!.enabled = false;
this.discardButton.enabled = false;
} catch (error) {
vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error));
@@ -187,7 +227,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
}));
// Discard
this.discardButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
this.discardButton = this.modelView.modelBuilder.button().withProps({
label: loc.discardText,
iconPath: IconPathHelper.discard,
enabled: false
@@ -195,15 +235,17 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.disposables.push(
this.discardButton.onDidClick(async () => {
this.discardButton!.enabled = false;
this.discardButton.enabled = false;
try {
this.editWorkerNodeCount();
this.editCores();
this.editMemory();
this.workerBox.value = this.currentConfiguration.workers!.toString();
this.workerCoresRequestBox.value = this.currentConfiguration.workerCoresRequest;
this.workerCoresLimitBox.value = this.currentConfiguration.workerCoresLimit;
this.workerMemoryRequestBox.value = this.currentConfiguration.workerMemoryRequest;
this.workerMemoryLimitBox.value = this.currentConfiguration.workerMemoryLimit;
} catch (error) {
vscode.window.showErrorMessage(loc.pageDiscardFailed(error));
} finally {
this.saveButton!.enabled = false;
this.saveButton.enabled = false;
}
}));
@@ -213,25 +255,27 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
]).component();
}
private initializeConfigurationBoxes() {
this.workerBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
private initializeConfigurationBoxes(): void {
// Worker node count
this.workerBox = this.modelView.modelBuilder.inputBox().withProps({
readOnly: false,
validationErrorMessage: loc.workerValidationErrorMessage,
inputType: 'number',
placeHolder: loc.loading
placeHolder: loc.loading,
required: true
}).component();
this.disposables.push(
this.workerBox.onTextChanged(() => {
if (!(this.handleOnTextChanged(this.workerBox!))) {
if (!this.saveValueToEdit(this.workerBox, this.currentConfiguration.workers!.toString())) {
this.saveArgs.workers = undefined;
} else {
this.saveArgs.workers = parseInt(this.workerBox!.value!);
this.saveArgs.workers = parseInt(this.workerBox.value!);
}
})
);
this.coresLimitBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
// Worker nodes cores request
this.workerCoresRequestBox = this.modelView.modelBuilder.inputBox().withProps({
readOnly: false,
min: 1,
inputType: 'number',
@@ -239,16 +283,19 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
}).component();
this.disposables.push(
this.coresLimitBox.onTextChanged(() => {
if (!(this.handleOnTextChanged(this.coresLimitBox!))) {
this.saveArgs.coresLimit = undefined;
this.workerCoresRequestBox.onTextChanged(() => {
if (!(this.saveValueToEdit(this.workerCoresRequestBox, this.currentConfiguration.workerCoresRequest!))) {
this.saveArgs.workerCoresRequest = undefined;
} else if (this.workerCoresRequestBox.value === '') {
this.saveArgs.workerCoresRequest = '""';
} else {
this.saveArgs.coresLimit = this.coresLimitBox!.value;
this.saveArgs.workerCoresRequest = this.workerCoresRequestBox.value;
}
})
);
this.coresRequestBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
// Worker nodes cores limit
this.workerCoresLimitBox = this.modelView.modelBuilder.inputBox().withProps({
readOnly: false,
min: 1,
inputType: 'number',
@@ -256,67 +303,152 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
}).component();
this.disposables.push(
this.coresRequestBox.onTextChanged(() => {
if (!(this.handleOnTextChanged(this.coresRequestBox!))) {
this.saveArgs.coresRequest = undefined;
this.workerCoresLimitBox.onTextChanged(() => {
if (!(this.saveValueToEdit(this.workerCoresLimitBox, this.currentConfiguration.workerCoresLimit!))) {
this.saveArgs.workerCoresLimit = undefined;
} else if (this.workerCoresLimitBox.value === '') {
this.saveArgs.workerCoresLimit = '""';
} else {
this.saveArgs.coresRequest = this.coresRequestBox!.value;
this.saveArgs.workerCoresLimit = this.workerCoresLimitBox.value;
}
})
);
this.memoryLimitBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
// Worker nodes memory request
this.workerMemoryRequestBox = this.modelView.modelBuilder.inputBox().withProps({
readOnly: false,
min: 0.25,
validationErrorMessage: loc.memoryLimitValidationErrorMessage,
inputType: 'number',
placeHolder: loc.loading
}).component();
this.disposables.push(
this.memoryLimitBox.onTextChanged(() => {
if (!(this.handleOnTextChanged(this.memoryLimitBox!))) {
this.saveArgs.memoryLimit = undefined;
this.workerMemoryRequestBox.onTextChanged(() => {
if (!(this.saveValueToEdit(this.workerMemoryRequestBox, this.currentConfiguration.workerMemoryRequest!))) {
this.saveArgs.workerMemoryRequest = undefined;
} else if (this.workerMemoryRequestBox.value === '') {
this.saveArgs.workerMemoryRequest = '""';
} else {
this.saveArgs.memoryLimit = this.memoryLimitBox!.value + 'Gi';
this.saveArgs.workerMemoryRequest = this.workerMemoryRequestBox.value + 'Gi';
}
})
);
this.memoryRequestBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
// Worker nodes memory limit
this.workerMemoryLimitBox = this.modelView.modelBuilder.inputBox().withProps({
readOnly: false,
min: 0.25,
validationErrorMessage: loc.memoryRequestValidationErrorMessage,
inputType: 'number',
placeHolder: loc.loading
}).component();
this.disposables.push(
this.memoryRequestBox.onTextChanged(() => {
if (!(this.handleOnTextChanged(this.memoryRequestBox!))) {
this.saveArgs.memoryRequest = undefined;
this.workerMemoryLimitBox.onTextChanged(() => {
if (!(this.saveValueToEdit(this.workerMemoryLimitBox, this.currentConfiguration.workerMemoryLimit!))) {
this.saveArgs.workerMemoryLimit = undefined;
} else if (this.workerMemoryLimitBox.value === '') {
this.saveArgs.workerMemoryLimit = '""';
} else {
this.saveArgs.memoryRequest = this.memoryRequestBox!.value + 'Gi';
this.saveArgs.workerMemoryLimit = this.workerMemoryLimitBox.value + 'Gi';
}
})
);
// Coordinator node cores request
this.coordinatorCoresRequestBox = this.modelView.modelBuilder.inputBox().withProps({
readOnly: false,
min: 1,
inputType: 'number',
placeHolder: loc.loading
}).component();
this.disposables.push(
this.coordinatorCoresRequestBox.onTextChanged(() => {
if (!(this.saveValueToEdit(this.coordinatorCoresRequestBox, this.currentConfiguration.coordinatorCoresRequest!))) {
this.saveArgs.coordinatorCoresRequest = undefined;
} else if (this.coordinatorCoresRequestBox.value === '') {
this.saveArgs.coordinatorCoresRequest = '""';
} else {
this.saveArgs.coordinatorCoresRequest = this.coordinatorCoresRequestBox.value;
}
})
);
// Coordinator node cores limit
this.coordinatorCoresLimitBox = this.modelView.modelBuilder.inputBox().withProps({
readOnly: false,
min: 1,
inputType: 'number',
placeHolder: loc.loading
}).component();
this.disposables.push(
this.coordinatorCoresLimitBox.onTextChanged(() => {
if (!(this.saveValueToEdit(this.coordinatorCoresLimitBox, this.currentConfiguration.coordinatorCoresLimit!))) {
this.saveArgs.coordinatorCoresLimit = undefined;
} else if (this.coordinatorCoresLimitBox.value === '') {
this.saveArgs.coordinatorCoresLimit = '""';
} else {
this.saveArgs.coordinatorCoresLimit = this.coordinatorCoresLimitBox.value;
}
})
);
// Coordinator node memory request
this.coordinatorMemoryRequestBox = this.modelView.modelBuilder.inputBox().withProps({
readOnly: false,
min: 0.25,
inputType: 'number',
placeHolder: loc.loading
}).component();
this.disposables.push(
this.coordinatorMemoryRequestBox.onTextChanged(() => {
if (!(this.saveValueToEdit(this.coordinatorMemoryRequestBox, this.currentConfiguration.coordinatorMemoryRequest!))) {
this.saveArgs.coordinatorMemoryRequest = undefined;
} else if (this.coordinatorMemoryRequestBox.value === '') {
this.saveArgs.coordinatorMemoryRequest = '""';
} else {
this.saveArgs.coordinatorMemoryRequest = this.coordinatorMemoryRequestBox.value + 'Gi';
}
})
);
// Coordinator node memory limit
this.coordinatorMemoryLimitBox = this.modelView.modelBuilder.inputBox().withProps({
readOnly: false,
min: 0.25,
inputType: 'number',
placeHolder: loc.loading
}).component();
this.disposables.push(
this.coordinatorMemoryLimitBox.onTextChanged(() => {
if (!(this.saveValueToEdit(this.coordinatorMemoryLimitBox, this.currentConfiguration.coordinatorMemoryLimit!))) {
this.saveArgs.coordinatorMemoryLimit = undefined;
} else if (this.coordinatorMemoryLimitBox.value === '') {
this.saveArgs.coordinatorMemoryLimit = '""';
} else {
this.saveArgs.coordinatorMemoryLimit = this.coordinatorMemoryLimitBox.value + 'Gi';
}
})
);
}
private createUserInputSection(): azdata.Component[] {
private createUserInputWorkerSection(): azdata.Component[] {
if (this._postgresModel.configLastUpdated) {
this.editWorkerNodeCount();
this.editCores();
this.editMemory();
this.editWorkerCores();
this.editWorkerMemory();
}
return [
this.createWorkerNodesSectionContainer(),
this.createCoresMemorySection(),
this.createConfigurationSectionContainer(loc.coresRequest, this.coresRequestBox!),
this.createConfigurationSectionContainer(loc.coresLimit, this.coresLimitBox!),
this.createConfigurationSectionContainer(loc.memoryRequest, this.memoryRequestBox!),
this.createConfigurationSectionContainer(loc.memoryLimit, this.memoryLimitBox!)
this.createCoresMemorySection(loc.configurationPerNode, loc.postgresConfigurationInformation), // use loc.workerNodesConfigurationInformation when coordinator section is included
this.createConfigurationSectionContainer(loc.coresRequest, this.workerCoresRequestBox),
this.createConfigurationSectionContainer(loc.coresLimit, this.workerCoresLimitBox),
this.createConfigurationSectionContainer(loc.memoryRequest, this.workerMemoryRequestBox),
this.createConfigurationSectionContainer(loc.memoryLimit, this.workerMemoryLimitBox)
];
}
@@ -330,7 +462,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
alignItems: 'center'
}).component();
const keyComponent = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
const keyComponent = this.modelView.modelBuilder.text().withProps({
value: loc.workerNodeCount,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
@@ -338,7 +470,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
const keyContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component();
keyContainer.addItem(keyComponent, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '15px' } });
const information = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
const information = this.modelView.modelBuilder.button().withProps({
iconPath: IconPathHelper.information,
title: loc.workerNodesInformation,
width: '15px',
@@ -350,13 +482,29 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
flexContainer.addItem(keyContainer, keyFlex);
const inputContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component();
inputContainer.addItem(this.workerBox!, { CSSStyles: { 'margin-bottom': '15px', 'min-width': '50px', 'max-width': '225px' } });
inputContainer.addItem(this.workerBox, { CSSStyles: { 'margin-bottom': '15px', 'min-width': '50px', 'max-width': '225px' } });
flexContainer.addItem(inputContainer, inputFlex);
return flexContainer;
}
private createUserInputCoordinatorSection(): azdata.Component[] {
if (this._postgresModel.configLastUpdated) {
this.editCoordinatorCores();
this.editCoordinatorMemory();
}
return [
this.createCoresMemorySection(loc.configuration, loc.coordinatorNodeConfigurationInformation),
this.createConfigurationSectionContainer(loc.coresRequest, this.coordinatorCoresRequestBox),
this.createConfigurationSectionContainer(loc.coresLimit, this.coordinatorCoresLimitBox),
this.createConfigurationSectionContainer(loc.memoryRequest, this.coordinatorMemoryRequestBox),
this.createConfigurationSectionContainer(loc.memoryLimit, this.coordinatorMemoryLimitBox)
];
}
private createConfigurationSectionContainer(key: string, input: azdata.Component): azdata.FlexContainer {
const inputFlex = { flex: '0 1 150px' };
const keyFlex = { flex: `0 1 250px` };
@@ -366,7 +514,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
alignItems: 'center'
}).component();
const keyComponent = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
const keyComponent = this.modelView.modelBuilder.text().withProps({
value: key,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
@@ -383,39 +531,43 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
return flexContainer;
}
private handleOnTextChanged(component: azdata.InputBoxComponent): boolean {
if ((!component.value)) {
// if there is no text found in the inputbox component return false
/**
* A function that determines if an input box's value should be considered or not.
* Triggers the save and discard buttons to become enabled depending on the value change.
*
* If new value is the same as value found in config, do not consider this new value for editing.
* If new value is invalid, do not consider this new value for editing and enable discard button.
* If value is valid and not equal to original value found in config, add this new value to be considered
* for editing and enable save/discard buttons.
*
* @param component The input box that had an onTextChanged event triggered.
* @param originalValue The value that was contained in the input box before user interaction.
* @return A boolean that reads true if the new value should be taken in for editing or not.
*/
private saveValueToEdit(component: azdata.InputBoxComponent, originalValue: string): boolean {
if (component.value === originalValue) {
return false;
} else if ((!component.valid)) {
// if value given by user is not valid enable discard button for user
// to clear all inputs and return false
this.discardButton!.enabled = true;
return false;
} else {
// if a valid value has been entered into the input box, enable save and discard buttons
// so that user could choose to either edit instance or clear all inputs
// return true
this.saveButton!.enabled = true;
this.discardButton!.enabled = true;
this.saveButton.enabled = true;
this.discardButton.enabled = true;
return true;
}
}
private editWorkerNodeCount() {
private editWorkerNodeCount(): void {
// scale.shards was renamed to scale.workers. Check both for backwards compatibility.
let scale = this._postgresModel.config?.spec.scale;
let currentWorkers = scale?.workers ?? scale?.shards ?? 0;
this.workerBox!.min = currentWorkers;
this.workerBox!.placeHolder = currentWorkers.toString();
this.workerBox!.value = '';
this.currentConfiguration.workers = scale?.workers ?? scale?.shards ?? 0;
this.workerBox.min = this.currentConfiguration.workers;
this.workerBox.placeHolder = '';
this.workerBox.value = this.currentConfiguration.workers.toString();
this.saveArgs.workers = undefined;
}
private createCoresMemorySection(): azdata.DivContainer {
private createCoresMemorySection(title: string, description: string): azdata.DivContainer {
const titleFlex = { flex: `0 1 250px` };
const flexContainer = this.modelView.modelBuilder.flexContainer().withLayout({
@@ -423,17 +575,17 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
alignItems: 'center'
}).component();
const titleComponent = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.configurationPerNode,
const titleComponent = this.modelView.modelBuilder.text().withProps({
value: title,
CSSStyles: { ...cssStyles.title, 'font-weight': 'bold', 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
const titleContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component();
titleContainer.addItem(titleComponent, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '15px' } });
const information = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
const information = this.modelView.modelBuilder.button().withProps({
iconPath: IconPathHelper.information,
title: loc.postgresConfigurationInformation,
title: description,
width: '15px',
height: '15px',
enabled: false
@@ -448,62 +600,108 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
return configurationSection;
}
private editCores() {
let currentCPUSize = this._postgresModel.config?.spec.scheduling?.default?.resources?.requests?.cpu;
if (!currentCPUSize) {
currentCPUSize = '';
private editWorkerCores(): void {
//Cores Request
this.currentConfiguration.workerCoresRequest = this._postgresModel.config?.spec.scheduling?.default?.resources?.requests?.cpu;
if (!this.currentConfiguration.workerCoresRequest) {
this.currentConfiguration.workerCoresRequest = '';
}
this.coresRequestBox!.validationErrorMessage = loc.validationMin(this.coresRequestBox!.min!);
this.coresRequestBox!.placeHolder = currentCPUSize;
this.coresRequestBox!.value = '';
this.saveArgs.coresRequest = undefined;
this.workerCoresRequestBox.placeHolder = '';
this.workerCoresRequestBox.value = this.currentConfiguration.workerCoresRequest;
this.saveArgs.workerCoresRequest = undefined;
currentCPUSize = this._postgresModel.config?.spec.scheduling?.default?.resources?.limits?.cpu;
if (!currentCPUSize) {
currentCPUSize = '';
// Cores Limit
this.currentConfiguration.workerCoresLimit = this._postgresModel.config?.spec.scheduling?.default?.resources?.limits?.cpu;
if (!this.currentConfiguration.workerCoresLimit) {
this.currentConfiguration.workerCoresLimit = '';
}
this.coresLimitBox!.validationErrorMessage = loc.validationMin(this.coresLimitBox!.min!);
this.coresLimitBox!.placeHolder = currentCPUSize;
this.coresLimitBox!.value = '';
this.saveArgs.coresLimit = undefined;
this.workerCoresLimitBox.placeHolder = '';
this.workerCoresLimitBox.value = this.currentConfiguration.workerCoresLimit;
this.saveArgs.workerCoresLimit = undefined;
}
private editMemory() {
let currentMemSizeConversion: string;
private editWorkerMemory(): void {
//Memory Request
let currentMemorySize = this._postgresModel.config?.spec.scheduling?.default?.resources?.requests?.memory;
if (!currentMemorySize) {
currentMemSizeConversion = '';
this.currentConfiguration.workerMemoryRequest = '';
} else {
currentMemSizeConversion = convertToGibibyteString(currentMemorySize);
this.currentConfiguration.workerMemoryRequest = convertToGibibyteString(currentMemorySize);
}
this.memoryRequestBox!.placeHolder = currentMemSizeConversion!;
this.memoryRequestBox!.value = '';
this.saveArgs.memoryRequest = undefined;
this.workerMemoryRequestBox.placeHolder = '';
this.workerMemoryRequestBox.value = this.currentConfiguration.workerMemoryRequest;
this.saveArgs.workerMemoryRequest = undefined;
//Memory Limit
currentMemorySize = this._postgresModel.config?.spec.scheduling?.default?.resources?.limits?.memory;
if (!currentMemorySize) {
currentMemSizeConversion = '';
this.currentConfiguration.workerMemoryLimit = '';
} else {
currentMemSizeConversion = convertToGibibyteString(currentMemorySize);
this.currentConfiguration.workerMemoryLimit = convertToGibibyteString(currentMemorySize);
}
this.memoryLimitBox!.placeHolder = currentMemSizeConversion!;
this.memoryLimitBox!.value = '';
this.saveArgs.memoryLimit = undefined;
this.workerMemoryLimitBox.placeHolder = '';
this.workerMemoryLimitBox.value = this.currentConfiguration.workerMemoryLimit;
this.saveArgs.workerMemoryLimit = undefined;
}
private handleServiceUpdated() {
private editCoordinatorCores(): void {
// TODO get current cpu size for coordinator
this.currentConfiguration.coordinatorCoresRequest = this._postgresModel.config?.spec.scheduling?.default?.resources?.requests?.cpu;
if (!this.currentConfiguration.coordinatorCoresRequest) {
this.currentConfiguration.coordinatorCoresRequest = '';
}
this.coordinatorCoresRequestBox.placeHolder = '';
this.coordinatorCoresRequestBox.value = this.currentConfiguration.coordinatorCoresRequest;
this.saveArgs.coordinatorCoresRequest = undefined;
// TODO get current cpu size for coordinator
this.currentConfiguration.coordinatorCoresLimit = this._postgresModel.config?.spec.scheduling?.default?.resources?.limits?.cpu;
if (!this.currentConfiguration.coordinatorCoresLimit) {
this.currentConfiguration.coordinatorCoresLimit = '';
}
this.coordinatorCoresLimitBox.placeHolder = '';
this.coordinatorCoresLimitBox.value = this.currentConfiguration.coordinatorCoresLimit;
this.saveArgs.coordinatorCoresLimit = undefined;
}
private editCoordinatorMemory(): void {
// TODO get current memory size for coordinator
let currentMemorySize = this._postgresModel.config?.spec.scheduling?.default?.resources?.requests?.memory;
if (!currentMemorySize) {
this.currentConfiguration.coordinatorCoresRequest = '';
} else {
this.currentConfiguration.coordinatorCoresRequest = convertToGibibyteString(currentMemorySize);
}
this.coordinatorMemoryRequestBox.placeHolder = '';
this.coordinatorMemoryRequestBox.value = this.currentConfiguration.coordinatorMemoryRequest;
this.saveArgs.coordinatorMemoryRequest = undefined;
// TODO get current memory size for coordinator
currentMemorySize = this._postgresModel.config?.spec.scheduling?.default?.resources?.limits?.memory;
if (!currentMemorySize) {
this.currentConfiguration.coordinatorCoresLimit = '';
} else {
this.currentConfiguration.coordinatorCoresLimit = convertToGibibyteString(currentMemorySize);
}
this.coordinatorMemoryLimitBox.placeHolder = '';
this.coordinatorMemoryLimitBox.value = this.currentConfiguration.coordinatorMemoryLimit;
this.saveArgs.coordinatorMemoryLimit = undefined;
}
private handleServiceUpdated(): void {
this.editWorkerNodeCount();
this.editCores();
this.editMemory();
this.editWorkerCores();
this.editWorkerMemory();
/* TODO perform once Coordinator section is in view
this.editCoordinatorCores();
this.editCoordinatorMemory(); */
}
}

View File

@@ -12,9 +12,10 @@ import { PostgresModel } from '../../../models/postgresModel';
export class PostgresConnectionStringsPage extends DashboardPage {
private keyValueContainer?: KeyValueContainer;
private connectionStringsLoading!: azdata.LoadingComponent;
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
super(modelView);
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _postgresModel: PostgresModel) {
super(modelView, dashboard);
this.disposables.push(this._postgresModel.onConfigUpdated(
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
@@ -59,7 +60,14 @@ export class PostgresConnectionStringsPage extends DashboardPage {
this.keyValueContainer = new KeyValueContainer(this.modelView.modelBuilder, this.getConnectionStrings());
this.disposables.push(this.keyValueContainer);
content.addItem(this.keyValueContainer.container);
this.connectionStringsLoading = this.modelView.modelBuilder.loadingComponent()
.withItem(this.keyValueContainer.container)
.withProperties<azdata.LoadingComponentProperties>({
loading: !this._postgresModel.configLastUpdated
}).component();
content.addItem(this.connectionStringsLoading, { CSSStyles: cssStyles.text });
this.initialized = true;
return root;
}
@@ -88,5 +96,6 @@ export class PostgresConnectionStringsPage extends DashboardPage {
private handleServiceUpdated() {
this.keyValueContainer?.refresh(this.getConnectionStrings());
this.connectionStringsLoading.loading = false;
}
}

View File

@@ -0,0 +1,67 @@
/*---------------------------------------------------------------------------------------------
* 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 loc from '../../../localizedConstants';
import { IconPathHelper } from '../../../constants';
import { PostgresParametersPage } from './postgresParameters';
import { PostgresModel, EngineSettingsModel } from '../../../models/postgresModel';
export class PostgresCoordinatorNodeParametersPage extends PostgresParametersPage {
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, postgresModel: PostgresModel) {
super(modelView, dashboard, postgresModel);
}
protected get title(): string {
return loc.coordinatorNodeParameters;
}
protected get id(): string {
return 'postgres-coordinator-node-parameters';
}
protected get icon(): { dark: string; light: string; } {
return IconPathHelper.gearGray;
}
protected get description(): string {
return loc.coordinatorNodeParametersDescription;
}
protected get engineSettings(): EngineSettingsModel[] {
return this._postgresModel.coordinatorNodeEngineSettings;
}
protected async saveParameterEdits(): Promise<void> {
/* TODO add correct azdata call for editing coordinator parameters
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{ engineSettings: engineSettings.toString() },
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session);
*/
}
protected async resetAllParameters(): Promise<void> {
/* TODO add correct azdata call for editing coordinator parameters
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{ engineSettings: `''`, replaceEngineSettings: true },
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session);
*/
}
protected async resetParameter(): Promise<void> {
/* TODO add correct azdata call for editing coordinator parameters
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{ engineSettings: parameterName + '=' },
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session);
*/
}
}

View File

@@ -14,8 +14,9 @@ import { Dashboard } from '../../components/dashboard';
import { PostgresDiagnoseAndSolveProblemsPage } from './postgresDiagnoseAndSolveProblemsPage';
import { PostgresSupportRequestPage } from './postgresSupportRequestPage';
import { PostgresComputeAndStoragePage } from './postgresComputeAndStoragePage';
import { PostgresParametersPage } from './postgresParametersPage';
import { PostgresWorkerNodeParametersPage } from './postgresWorkerNodeParametersPage';
import { PostgresPropertiesPage } from './postgresPropertiesPage';
import { PostgresResourceHealthPage } from './postgresResourceHealthPage';
export class PostgresDashboard extends Dashboard {
constructor(private _context: vscode.ExtensionContext, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
@@ -31,13 +32,16 @@ export class PostgresDashboard extends Dashboard {
}
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._postgresModel);
const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._postgresModel);
const computeAndStoragePage = new PostgresComputeAndStoragePage(modelView, this._postgresModel);
const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel);
const parametersPage = new PostgresParametersPage(modelView, this._postgresModel);
const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this._context, this._postgresModel);
const supportRequestPage = new PostgresSupportRequestPage(modelView, this._controllerModel, this._postgresModel);
const overviewPage = new PostgresOverviewPage(modelView, this.dashboard, this._controllerModel, this._postgresModel);
const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this.dashboard, this._postgresModel);
const computeAndStoragePage = new PostgresComputeAndStoragePage(modelView, this.dashboard, this._postgresModel);
const propertiesPage = new PostgresPropertiesPage(modelView, this.dashboard, this._controllerModel, this._postgresModel);
// TODO Add dashboard once backend is able to be connected for per role server parameter edits.
// const coordinatorNodeParametersPage = new PostgresCoordinatorNodeParametersPage(modelView, this._postgresModel);
const workerNodeParametersPage = new PostgresWorkerNodeParametersPage(modelView, this.dashboard, this._postgresModel);
const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this.dashboard, this._context, this._controllerModel, this._postgresModel);
const supportRequestPage = new PostgresSupportRequestPage(modelView, this.dashboard, this._controllerModel, this._postgresModel);
const resourceHealthPage = new PostgresResourceHealthPage(modelView, this.dashboard, this._postgresModel);
return [
overviewPage.tab,
@@ -47,12 +51,13 @@ export class PostgresDashboard extends Dashboard {
propertiesPage.tab,
connectionStringsPage.tab,
computeAndStoragePage.tab,
parametersPage.tab
workerNodeParametersPage.tab
]
},
{
title: loc.supportAndTroubleshooting,
tabs: [
resourceHealthPage.tab,
diagnoseAndSolveProblemsPage.tab,
supportRequestPage.tab
]

View File

@@ -9,10 +9,11 @@ import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
import { PostgresModel } from '../../../models/postgresModel';
import { ControllerModel } from '../../../models/controllerModel';
export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage {
constructor(protected modelView: azdata.ModelView, private _context: vscode.ExtensionContext, private _postgresModel: PostgresModel) {
super(modelView);
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _context: vscode.ExtensionContext, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView, dashboard);
}
protected get title(): string {
@@ -50,9 +51,8 @@ export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage {
this.disposables.push(
troubleshootButton.onDidClick(() => {
process.env['POSTGRES_SERVER_NAMESPACE'] = this._postgresModel.config?.metadata.namespace;
process.env['POSTGRES_SERVER_NAMESPACE'] = this._controllerModel.controllerConfig?.metadata.namespace ?? '';
process.env['POSTGRES_SERVER_NAME'] = this._postgresModel.info.name;
process.env['POSTGRES_SERVER_VERSION'] = this._postgresModel.engineVersion;
vscode.commands.executeCommand('bookTreeView.openBook', this._context.asAbsolutePath('notebooks/arcDataServices'), true, 'postgres/tsg100-troubleshoot-postgres');
}));

View File

@@ -7,27 +7,38 @@ import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants';
import { IconPathHelper, cssStyles, iconSize } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
import { ControllerModel } from '../../../models/controllerModel';
import { PostgresModel } from '../../../models/postgresModel';
import { promptAndConfirmPassword, promptForInstanceDeletion } from '../../../common/utils';
import { ResourceType } from 'arc';
export type PodStatusModel = {
podName: azdata.Component,
type: string,
status: string
};
export class PostgresOverviewPage extends DashboardPage {
private propertiesLoading!: azdata.LoadingComponent;
private serverGroupNodesLoading!: azdata.LoadingComponent;
private kibanaLoading!: azdata.LoadingComponent;
private grafanaLoading!: azdata.LoadingComponent;
private properties!: azdata.PropertiesContainerComponent;
private kibanaLink!: azdata.HyperlinkComponent;
private grafanaLink!: azdata.HyperlinkComponent;
private deleteButton!: azdata.ButtonComponent;
private podStatusTable!: azdata.DeclarativeTableComponent;
private podStatusData: PodStatusModel[] = [];
private readonly _azdataApi: azdataExt.IExtension;
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView);
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView, dashboard);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this.disposables.push(
@@ -132,8 +143,63 @@ export class PostgresOverviewPage extends DashboardPage {
[loc.kibanaDashboard, this.kibanaLoading, loc.kibanaDashboardDescription],
[loc.grafanaDashboard, this.grafanaLoading, loc.grafanaDashboardDescription]]
}).component();
content.addItem(endpointsTable);
// Server Group Nodes
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.serverGroupNodes,
CSSStyles: titleCSS
}).component());
this.podStatusTable = this.modelView.modelBuilder.declarativeTable().withProps({
width: '100%',
columns: [
{
displayName: loc.name,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: '35%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: {
...cssStyles.tableRow,
'overflow': 'hidden',
'text-overflow': 'ellipsis',
'white-space': 'nowrap',
'max-width': '0'
}
},
{
displayName: loc.type,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: '35%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
},
{
displayName: loc.status,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: '30%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
}
],
data: [this.podStatusData.map(p => [p.podName, p.type, p.status])]
}).component();
this.serverGroupNodesLoading = this.modelView.modelBuilder.loadingComponent()
.withItem(this.podStatusTable)
.withProperties<azdata.LoadingComponentProperties>({
loading: !this._postgresModel.configLastUpdated
}).component();
this.refreshServerNodes();
content.addItem(this.serverGroupNodesLoading, { CSSStyles: cssStyles.text });
this.initialized = true;
return root;
}
@@ -151,21 +217,13 @@ export class PostgresOverviewPage extends DashboardPage {
try {
const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : '');
if (password) {
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
try {
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{
adminPassword: true,
noWait: true
},
this._postgresModel.engineVersion,
Object.assign({ 'AZDATA_PASSWORD': password }, this._controllerModel.azdataAdditionalEnvVars),
session
);
} finally {
session.dispose();
}
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{
adminPassword: true,
noWait: true
},
Object.assign({ 'AZDATA_PASSWORD': password }, this._controllerModel.azdataAdditionalEnvVars));
vscode.window.showInformationMessage(loc.passwordReset);
}
} catch (error) {
@@ -176,14 +234,14 @@ export class PostgresOverviewPage extends DashboardPage {
}));
// Delete service
const deleteButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
this.deleteButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
label: loc.deleteText,
iconPath: IconPathHelper.delete
}).component();
this.disposables.push(
deleteButton.onDidClick(async () => {
deleteButton.enabled = false;
this.deleteButton.onDidClick(async () => {
this.deleteButton.enabled = false;
try {
if (await promptForInstanceDeletion(this._postgresModel.info.name)) {
await vscode.window.withProgress(
@@ -193,22 +251,23 @@ export class PostgresOverviewPage extends DashboardPage {
cancellable: false
},
async (_progress, _token) => {
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
try {
return await this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name, this._controllerModel.azdataAdditionalEnvVars, session);
} finally {
session.dispose();
}
return await this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name, this._controllerModel.azdataAdditionalEnvVars, this._controllerModel.controllerContext);
}
);
await this._controllerModel.refreshTreeNode();
vscode.window.showInformationMessage(loc.instanceDeleted(this._postgresModel.info.name));
try {
await this.dashboard.close();
} catch (err) {
// Failures closing the dashboard aren't something we need to show users
console.log('Error closing Arc Postgres dashboard ', err);
}
}
} catch (error) {
vscode.window.showErrorMessage(loc.instanceDeletionFailed(this._postgresModel.info.name, error));
} finally {
deleteButton.enabled = true;
this.deleteButton.enabled = true;
}
}));
@@ -223,6 +282,7 @@ export class PostgresOverviewPage extends DashboardPage {
refreshButton.enabled = false;
try {
this.propertiesLoading!.loading = true;
this.serverGroupNodesLoading!.loading = true;
this.kibanaLoading!.loading = true;
this.grafanaLoading!.loading = true;
@@ -257,7 +317,7 @@ export class PostgresOverviewPage extends DashboardPage {
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
{ component: resetPasswordButton },
{ component: deleteButton },
{ component: this.deleteButton },
{ component: refreshButton, toolbarSeparatorAfter: true },
{ component: openInAzurePortalButton }
]).component();
@@ -281,6 +341,54 @@ export class PostgresOverviewPage extends DashboardPage {
];
}
private getPodStatus(): PodStatusModel[] {
let podModels: PodStatusModel[] = [];
const podStatus = this._postgresModel.config?.status.podsStatus;
podStatus?.forEach(p => {
// If a condition of the pod has a status of False, pod is not Ready
const status = p.conditions.find(c => c.status === 'False') ? loc.notReady : loc.ready;
const podLabelContainer = this.modelView.modelBuilder.flexContainer().withProps({
CSSStyles: { 'alignItems': 'center', 'height': '15px' }
}).component();
const imageComponent = this.modelView.modelBuilder.image().withProps({
iconPath: IconPathHelper.postgres,
width: iconSize,
height: iconSize,
iconHeight: '15px',
iconWidth: '15px'
}).component();
let podLabel = this.modelView.modelBuilder.text().withProps({
value: p.name,
}).component();
if (p.role.toUpperCase() === loc.worker.toUpperCase()) {
podLabelContainer.addItem(imageComponent, { CSSStyles: { 'margin-left': '15px', 'margin-right': '0px' } });
podLabelContainer.addItem(podLabel);
let pod: PodStatusModel = {
podName: podLabelContainer,
type: loc.worker,
status: status
};
podModels.push(pod);
} else {
podLabelContainer.addItem(imageComponent, { CSSStyles: { 'margin-right': '0px' } });
podLabelContainer.addItem(podLabel);
let pod: PodStatusModel = {
podName: podLabelContainer,
type: loc.coordinator,
status: status
};
podModels.unshift(pod);
}
});
return podModels;
}
private refreshDashboardLinks(): void {
if (this._postgresModel.config) {
const kibanaUrl = this._postgresModel.config.status.logSearchDashboard ?? '';
@@ -295,6 +403,14 @@ export class PostgresOverviewPage extends DashboardPage {
}
}
private refreshServerNodes(): void {
if (this._postgresModel.config) {
this.podStatusData = this.getPodStatus();
this.podStatusTable.data = this.podStatusData.map(p => [p.podName, p.type, p.status]);
this.serverGroupNodesLoading.loading = false;
}
}
private handleRegistrationsUpdated() {
this.properties!.propertyItems = this.getProperties();
this.propertiesLoading!.loading = false;
@@ -304,5 +420,6 @@ export class PostgresOverviewPage extends DashboardPage {
this.properties!.propertyItems = this.getProperties();
this.propertiesLoading!.loading = false;
this.refreshDashboardLinks();
this.refreshServerNodes();
}
}

View File

@@ -0,0 +1,626 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as loc from '../../../localizedConstants';
import { UserCancelledError } from '../../../common/api';
import { IconPathHelper, cssStyles } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
import { EngineSettingsModel, PostgresModel } from '../../../models/postgresModel';
import { debounce } from '../../../common/utils';
export type ParametersModel = {
parameterName: string,
originalValue: string,
valueComponent: azdata.TextComponent | azdata.DropDownComponent | azdata.CheckBoxComponent,
information?: azdata.ButtonComponent,
description: string,
resetButton: azdata.ButtonComponent
};
export abstract class PostgresParametersPage extends DashboardPage {
private searchBox!: azdata.InputBoxComponent;
protected _parametersTable!: azdata.DeclarativeTableComponent;
private parameterContainer!: azdata.DivContainer;
private parametersTableLoading?: azdata.LoadingComponent;
private discardButton!: azdata.ButtonComponent;
private saveButton!: azdata.ButtonComponent;
private resetAllButton!: azdata.ButtonComponent;
private connectToServerButton?: azdata.ButtonComponent;
protected _parameters: ParametersModel[] = [];
private changedComponentValues: Set<string> = new Set();
private parameterUpdates: Map<string, string> = new Map();
protected readonly _azdataApi: azdataExt.IExtension;
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, protected _postgresModel: PostgresModel) {
super(modelView, dashboard);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this.initializeSearchBox();
this.disposables.push(
this._postgresModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())),
this._postgresModel.onEngineSettingsUpdated(() => this.eventuallyRunOnInitialized(() => this.refreshParametersTable()))
);
}
protected abstract get description(): string;
protected abstract get engineSettings(): EngineSettingsModel[];
protected get container(): azdata.Component {
const root = this.modelView.modelBuilder.divContainer().component();
const content = this.modelView.modelBuilder.divContainer().component();
root.addItem(content, { CSSStyles: { 'margin': '20px' } });
content.addItem(this.modelView.modelBuilder.text().withProps({
value: this.title,
CSSStyles: { ...cssStyles.title }
}).component());
content.addItem(this.modelView.modelBuilder.text().withProps({
value: this.description,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component());
content.addItem(this.modelView.modelBuilder.hyperlink().withProps({
label: loc.learnAboutNodeParameters,
url: 'https://docs.microsoft.com/azure/azure-arc/data/configure-server-parameters-postgresql-hyperscale'
}).component(), { CSSStyles: { 'margin-bottom': '20px' } });
content.addItem(this.searchBox!, { CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'margin-bottom': '20px' } });
this._parametersTable = this.modelView.modelBuilder.declarativeTable().withProps({
width: '100%',
columns: [
{
displayName: loc.parameterName,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: '20%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
},
{
displayName: loc.value,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: false,
width: '20%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
},
{
displayName: loc.description,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: '50%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: {
...cssStyles.tableRow
}
},
{
displayName: loc.resetToDefault,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: false,
width: '10%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
}
],
data: []
}).component();
this.parameterContainer = this.modelView.modelBuilder.divContainer().component();
this.selectComponent();
content.addItem(this.parameterContainer);
this.initialized = true;
return root;
}
protected get toolbarContainer(): azdata.ToolbarContainer {
// Save Edits
this.saveButton = this.modelView.modelBuilder.button().withProps({
label: loc.saveText,
iconPath: IconPathHelper.save,
enabled: false
}).component();
let engineSettings: string[] = [];
this.disposables.push(
this.saveButton.onDidClick(async () => {
this.saveButton.enabled = false;
try {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.updatingInstance(this._postgresModel.info.name),
cancellable: false
},
async (_progress, _token): Promise<void> => {
try {
this.parameterUpdates.forEach((value, key) => {
engineSettings.push(`${key}="${value}"`);
});
await this.saveParameterEdits(engineSettings.toString());
} catch (err) {
// If an error occurs while editing the instance then re-enable the save button since
// the edit wasn't successfully applied
this.saveButton.enabled = true;
throw err;
}
try {
await this._postgresModel.refresh();
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
}
}
);
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
engineSettings = [];
this.changedComponentValues.clear();
this.parameterUpdates.clear();
this.discardButton.enabled = false;
this.resetAllButton.enabled = true;
} catch (error) {
vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error));
}
})
);
// Discard
this.discardButton = this.modelView.modelBuilder.button().withProps({
label: loc.discardText,
iconPath: IconPathHelper.discard,
enabled: false
}).component();
this.disposables.push(
this.discardButton.onDidClick(async () => {
this.discardButton.enabled = false;
try {
this.discardParametersTableChanges();
} catch (error) {
this.discardButton!.enabled = true;
vscode.window.showErrorMessage(loc.pageDiscardFailed(error));
} finally {
this.changedComponentValues.clear();
this.saveButton.enabled = false;
this.parameterUpdates.clear();
}
})
);
// Reset all
this.resetAllButton = this.modelView.modelBuilder.button().withProps({
label: loc.resetAllToDefault,
iconPath: IconPathHelper.reset,
enabled: false
}).component();
this.disposables.push(
this.resetAllButton.onDidClick(async () => {
this.resetAllButton.enabled = false;
this.discardButton.enabled = false;
this.saveButton.enabled = false;
try {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.updatingInstance(this._postgresModel.info.name),
cancellable: false
},
async (_progress, _token): Promise<void> => {
try {
await this.resetAllParameters();
} catch (err) {
// If an error occurs while resetting the instance then re-enable the reset button since
// the edit wasn't successfully applied
if (this.parameterUpdates.size > 0) {
this.discardButton.enabled = true;
this.saveButton.enabled = true;
}
this.resetAllButton.enabled = true;
throw err;
}
this.changedComponentValues.clear();
try {
await this._postgresModel.refresh();
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
}
}
);
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
this.parameterUpdates.clear();
} catch (error) {
vscode.window.showErrorMessage(loc.resetFailed(error));
}
})
);
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
{ component: this.saveButton },
{ component: this.discardButton },
{ component: this.resetAllButton }
]).component();
}
protected initializeConnectButton(): void {
this.connectToServerButton = this.modelView.modelBuilder.button().withProps({
label: loc.connectToServer,
enabled: false,
CSSStyles: { 'max-width': '125px' }
}).component();
this.disposables.push(
this.connectToServerButton.onDidClick(async () => {
this.connectToServerButton!.enabled = false;
if (!vscode.extensions.getExtension(loc.postgresExtension)) {
const response = await vscode.window.showErrorMessage(loc.missingExtension('PostgreSQL'), loc.yes, loc.no);
if (response !== loc.yes) {
this.connectToServerButton!.enabled = true;
return;
}
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.installingExtension(loc.postgresExtension),
cancellable: false
},
async (_progress, _token): Promise<void> => {
try {
await vscode.commands.executeCommand('workbench.extensions.installExtension', loc.postgresExtension);
} catch (err) {
vscode.window.showErrorMessage(loc.extensionInstallationFailed(loc.postgresExtension));
this.connectToServerButton!.enabled = true;
throw err;
}
}
);
vscode.window.showInformationMessage(loc.extensionInstalled(loc.postgresExtension));
}
this.parametersTableLoading!.loading = true;
await this.callGetEngineSettings().finally(() => this.parametersTableLoading!.loading = false);
this.searchBox.enabled = true;
this.resetAllButton.enabled = true;
this.parameterContainer.clearItems();
this.parameterContainer.addItem(this._parametersTable);
})
);
}
private selectComponent(): void {
if (!this._postgresModel.engineSettingsLastUpdated) {
this.parameterContainer.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.connectToPostgresDescription,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component());
this.initializeConnectButton();
this.parameterContainer.addItem(this.connectToServerButton!, { CSSStyles: { 'max-width': '125px' } });
this.parametersTableLoading = this.modelView.modelBuilder.loadingComponent().component();
this.parameterContainer.addItem(this.parametersTableLoading);
} else {
this.searchBox.enabled = true;
this.resetAllButton.enabled = true;
this.parameterContainer.addItem(this._parametersTable!);
this.refreshParametersTable();
}
}
private async callGetEngineSettings(): Promise<void> {
try {
await this._postgresModel.getEngineSettings();
} catch (error) {
if (error instanceof UserCancelledError) {
vscode.window.showWarningMessage(loc.pgConnectionRequired);
} else {
vscode.window.showErrorMessage(loc.fetchEngineSettingsFailed(this._postgresModel.info.name, error));
}
this.connectToServerButton!.enabled = true;
throw error;
}
}
protected initializeSearchBox(): void {
this.searchBox = this.modelView.modelBuilder.inputBox().withProps({
readOnly: false,
enabled: false,
placeHolder: loc.searchToFilter
}).component();
this.disposables.push(
this.searchBox.onTextChanged(() => {
this.onSearchFilter();
})
);
}
@debounce(500)
private onSearchFilter(): void {
if (!this.searchBox.value) {
this._parametersTable.setFilter(undefined);
} else {
this.filterParameters(this.searchBox.value);
}
}
private filterParameters(search: string): void {
const filteredRowIndexes: number[] = [];
this._parametersTable.data?.forEach((row, index) => {
if (row[0].toUpperCase()?.search(search.toUpperCase()) !== -1 || row[2].toUpperCase()?.search(search.toUpperCase()) !== -1) {
filteredRowIndexes.push(index);
}
});
this._parametersTable.setFilter(filteredRowIndexes);
}
private handleOnTextChanged(component: azdata.InputBoxComponent, name: string, currentValue: string | undefined): boolean {
if (!component.valid) {
// If invalid value return false and enable discard button
this.discardButton.enabled = true;
this.collectChangedComponents(name);
return false;
} else if (component.value === currentValue) {
this.removeFromChangedComponents(name);
return false;
} else {
/* If a valid value has been entered into the input box, enable save and discard buttons
so that user could choose to either edit instance or clear all inputs
return true */
this.saveButton.enabled = true;
this.discardButton.enabled = true;
this.collectChangedComponents(name);
return true;
}
}
protected createParameterComponents(engineSetting: EngineSettingsModel): ParametersModel {
// Can reset individual parameter
const resetParameterButton = this.modelView.modelBuilder.button().withProps({
iconPath: IconPathHelper.reset,
title: loc.resetToDefault,
width: '20px',
height: '20px',
enabled: true
}).component();
this.disposables.push(
resetParameterButton.onDidClick(async () => {
try {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.updatingInstance(this._postgresModel.info.name),
cancellable: false
},
async (_progress, _token): Promise<void> => {
await this.resetParameter(engineSetting.parameterName!);
try {
await this._postgresModel.refresh();
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
}
}
);
this.removeFromChangedComponents(engineSetting.parameterName!);
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
} catch (error) {
vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error));
}
})
);
let valueComponent: azdata.Component;
if (engineSetting.type === 'enum') {
// If type is enum, component should be drop down menu
let options = engineSetting.options?.slice(1, -1).split(',');
let values: string[] = [];
options!.forEach(option => {
values.push(option.slice(option.indexOf('"') + 1, -1));
});
let valueBox = this.modelView.modelBuilder.dropDown().withProps({
values: values,
value: engineSetting.value,
width: '150px'
}).component();
valueComponent = valueBox;
this.disposables.push(
valueBox.onValueChanged(() => {
if (engineSetting.value !== String(valueBox.value)) {
this.parameterUpdates.set(engineSetting.parameterName!, String(valueBox.value));
this.collectChangedComponents(engineSetting.parameterName!);
this.saveButton.enabled = true;
this.discardButton.enabled = true;
} else if (this.parameterUpdates.has(engineSetting.parameterName!)) {
this.parameterUpdates.delete(engineSetting.parameterName!);
this.removeFromChangedComponents(engineSetting.parameterName!);
}
})
);
} else if (engineSetting.type === 'bool') {
// If type is bool, component should be checkbox to turn on or off
let valueBox = this.modelView.modelBuilder.checkBox().withProps({
label: loc.on,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
valueComponent = valueBox;
if (engineSetting.value === 'on') {
valueBox.checked = true;
} else {
valueBox.checked = false;
}
this.disposables.push(
valueBox.onChanged(() => {
if (valueBox.checked && engineSetting.value === 'off') {
this.parameterUpdates.set(engineSetting.parameterName!, loc.on);
this.collectChangedComponents(engineSetting.parameterName!);
this.saveButton.enabled = true;
this.discardButton.enabled = true;
} else if (!valueBox.checked && engineSetting.value === 'on') {
this.parameterUpdates.set(engineSetting.parameterName!, loc.off);
this.collectChangedComponents(engineSetting.parameterName!);
this.saveButton.enabled = true;
this.discardButton.enabled = true;
} else if (this.parameterUpdates.has(engineSetting.parameterName!)) {
this.parameterUpdates.delete(engineSetting.parameterName!);
this.removeFromChangedComponents(engineSetting.parameterName!);
}
})
);
} else if (engineSetting.type === 'string') {
// If type is string, component should be text inputbox
let valueBox = this.modelView.modelBuilder.inputBox().withProps({
required: true,
readOnly: false,
value: engineSetting.value,
width: '150px'
}).component();
valueComponent = valueBox;
this.disposables.push(
valueBox.onTextChanged(() => {
if ((this.handleOnTextChanged(valueBox, engineSetting.parameterName!, engineSetting.value))) {
this.parameterUpdates.set(engineSetting.parameterName!, `"${valueBox.value!}"`);
} else if (this.parameterUpdates.has(engineSetting.parameterName!)) {
this.parameterUpdates.delete(engineSetting.parameterName!);
}
})
);
} else {
// If type is real or interger, component should be inputbox set to inputType of number. Max and min values also set.
let valueBox = this.modelView.modelBuilder.inputBox().withProps({
required: true,
readOnly: false,
min: parseInt(engineSetting.min!),
max: parseInt(engineSetting.max!),
inputType: 'number',
value: engineSetting.value,
width: '150px'
}).component();
valueComponent = valueBox;
this.disposables.push(
valueBox.onTextChanged(() => {
if ((this.handleOnTextChanged(valueBox, engineSetting.parameterName!, engineSetting.value))) {
this.parameterUpdates.set(engineSetting.parameterName!, valueBox.value!);
} else if (this.parameterUpdates.has(engineSetting.parameterName!)) {
this.parameterUpdates.delete(engineSetting.parameterName!);
}
})
);
// Information bubble title to show allowed values
let information = this.modelView.modelBuilder.button().withProps({
iconPath: IconPathHelper.information,
width: '15px',
height: '15px',
enabled: false,
title: loc.rangeSetting(engineSetting.min!, engineSetting.max!)
}).component();
return {
parameterName: engineSetting.parameterName!,
originalValue: engineSetting.value!,
valueComponent: valueComponent,
information: information,
description: engineSetting.description!,
resetButton: resetParameterButton
};
}
return {
parameterName: engineSetting.parameterName!,
originalValue: engineSetting.value!,
valueComponent: valueComponent,
description: engineSetting.description!,
resetButton: resetParameterButton
};
}
private collectChangedComponents(name: string): void {
if (!this.changedComponentValues.has(name)) {
this.changedComponentValues.add(name);
}
}
private removeFromChangedComponents(name: string): void {
if (this.changedComponentValues.has(name)) {
this.changedComponentValues.delete(name);
}
}
private discardParametersTableChanges(): void {
let instanceOfCheckBox = function (object: any): object is azdata.CheckBoxComponent {
return 'checked' in object;
};
this.changedComponentValues.forEach(v => {
let param = this._parameters.find(p => p.parameterName === v);
if (instanceOfCheckBox(param!.valueComponent)) {
if (param!.originalValue === 'on') {
param!.valueComponent.checked = true;
} else {
param!.valueComponent.checked = false;
}
} else {
param!.valueComponent.value = param!.originalValue;
}
});
}
private refreshParametersTable(): void {
this._parameters = this.engineSettings.map(parameter => this.createParameterComponents(parameter));
this._parametersTable.data = this._parameters.map(p => {
if (p.information) {
// Container to hold input component and information bubble
const valueContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component();
valueContainer.addItem(p.valueComponent, { CSSStyles: { 'margin-right': '0px' } });
valueContainer.addItem(p.information, { CSSStyles: { 'margin-left': '5px' } });
return [p.parameterName, valueContainer, p.description, p.resetButton];
} else {
return [p.parameterName, p.valueComponent, p.description, p.resetButton];
}
});
}
private async handleServiceUpdated(): Promise<void> {
if (this._postgresModel.configLastUpdated && !this._postgresModel.engineSettingsLastUpdated) {
this.connectToServerButton!.enabled = true;
this.parametersTableLoading!.loading = false;
} else if (this._postgresModel.engineSettingsLastUpdated) {
await this.callGetEngineSettings();
this.discardButton.enabled = false;
this.saveButton.enabled = false;
}
}
protected abstract saveParameterEdits(engineSettings: string): Promise<void>;
protected abstract resetAllParameters(): Promise<void>;
protected abstract resetParameter(parameterName: string): Promise<void>;
}

View File

@@ -1,588 +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 vscode from 'vscode';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as loc from '../../../localizedConstants';
import { UserCancelledError } from '../../../common/api';
import { IconPathHelper, cssStyles } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
import { EngineSettingsModel, PostgresModel } from '../../../models/postgresModel';
import { debounce } from '../../../common/utils';
export type ParametersModel = {
parameterName: string,
valueContainer: azdata.FlexContainer,
description: string,
resetButton: azdata.ButtonComponent
};
export class PostgresParametersPage extends DashboardPage {
private searchBox!: azdata.InputBoxComponent;
private parametersTable!: azdata.DeclarativeTableComponent;
private parameterContainer?: azdata.DivContainer;
private _parametersTableLoading!: azdata.LoadingComponent;
private discardButton!: azdata.ButtonComponent;
private saveButton!: azdata.ButtonComponent;
private resetAllButton!: azdata.ButtonComponent;
private connectToServerButton?: azdata.ButtonComponent;
private _parameters: ParametersModel[] = [];
private parameterUpdates: Map<string, string> = new Map();
private readonly _azdataApi: azdataExt.IExtension;
constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) {
super(modelView);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this.initializeConnectButton();
this.initializeSearchBox();
this.disposables.push(
this._postgresModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())),
this._postgresModel.onEngineSettingsUpdated(() => this.eventuallyRunOnInitialized(() => this.refreshParametersTable()))
);
}
protected get title(): string {
return loc.nodeParameters;
}
protected get id(): string {
return 'postgres-node-parameters';
}
protected get icon(): { dark: string; light: string; } {
return IconPathHelper.gear;
}
protected get container(): azdata.Component {
const root = this.modelView.modelBuilder.divContainer().component();
const content = this.modelView.modelBuilder.divContainer().component();
root.addItem(content, { CSSStyles: { 'margin': '20px' } });
content.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.nodeParameters,
CSSStyles: { ...cssStyles.title }
}).component());
content.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.nodeParametersDescription,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component());
content.addItem(this.modelView.modelBuilder.hyperlink().withProps({
label: loc.learnAboutNodeParameters,
url: 'https://docs.microsoft.com/azure/azure-arc/data/configure-server-parameters-postgresql-hyperscale'
}).component(), { CSSStyles: { 'margin-bottom': '20px' } });
content.addItem(this.searchBox!, { CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'margin-bottom': '20px' } });
this.parametersTable = this.modelView.modelBuilder.declarativeTable().withProps({
width: '100%',
columns: [
{
displayName: loc.parameterName,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: '20%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
},
{
displayName: loc.value,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: false,
width: '20%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
},
{
displayName: loc.description,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: '50%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: {
...cssStyles.tableRow
}
},
{
displayName: loc.resetToDefault,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: false,
width: '10%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
}
],
data: []
}).component();
this._parametersTableLoading = this.modelView.modelBuilder.loadingComponent().component();
this.parameterContainer = this.modelView.modelBuilder.divContainer().component();
this.selectComponent();
content.addItem(this.parameterContainer);
this.initialized = true;
return root;
}
protected get toolbarContainer(): azdata.ToolbarContainer {
// Save Edits
this.saveButton = this.modelView.modelBuilder.button().withProps({
label: loc.saveText,
iconPath: IconPathHelper.save,
enabled: false
}).component();
let engineSettings: string[] = [];
this.disposables.push(
this.saveButton.onDidClick(async () => {
this.saveButton!.enabled = false;
try {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.updatingInstance(this._postgresModel.info.name),
cancellable: false
},
async (_progress, _token): Promise<void> => {
try {
this.parameterUpdates!.forEach((value, key) => {
engineSettings.push(`${key}="${value}"`);
});
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
try {
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{ engineSettings: engineSettings.toString() },
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session);
} finally {
session.dispose();
}
} catch (err) {
// If an error occurs while editing the instance then re-enable the save button since
// the edit wasn't successfully applied
this.saveButton!.enabled = true;
throw err;
}
await this._postgresModel.refresh();
}
);
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
engineSettings = [];
this.parameterUpdates!.clear();
this.discardButton!.enabled = false;
this.resetAllButton!.enabled = true;
} catch (error) {
vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error));
}
})
);
// Discard
this.discardButton = this.modelView.modelBuilder.button().withProps({
label: loc.discardText,
iconPath: IconPathHelper.discard,
enabled: false
}).component();
this.disposables.push(
this.discardButton.onDidClick(async () => {
this.discardButton!.enabled = false;
try {
this.refreshParametersTable();
} catch (error) {
vscode.window.showErrorMessage(loc.pageDiscardFailed(error));
} finally {
this.saveButton!.enabled = false;
}
})
);
// Reset all
this.resetAllButton = this.modelView.modelBuilder.button().withProps({
label: loc.resetAllToDefault,
iconPath: IconPathHelper.reset,
enabled: false
}).component();
this.disposables.push(
this.resetAllButton.onDidClick(async () => {
this.resetAllButton!.enabled = false;
this.discardButton!.enabled = false;
this.saveButton!.enabled = false;
try {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.updatingInstance(this._postgresModel.info.name),
cancellable: false
},
async (_progress, _token): Promise<void> => {
//all
// azdata arc postgres server edit -n <server group name> -e '' -re
let session: azdataExt.AzdataSession | undefined = undefined;
try {
session = await this._postgresModel.controllerModel.acquireAzdataSession();
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{ engineSettings: `''`, replaceEngineSettings: true },
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session);
} catch (err) {
// If an error occurs while resetting the instance then re-enable the reset button since
// the edit wasn't successfully applied
if (this.parameterUpdates.size > 0) {
this.discardButton!.enabled = true;
this.saveButton!.enabled = true;
}
this.resetAllButton!.enabled = true;
throw err;
} finally {
session?.dispose();
}
await this._postgresModel.refresh();
}
);
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
this.parameterUpdates!.clear();
} catch (error) {
vscode.window.showErrorMessage(loc.resetFailed(error));
}
})
);
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
{ component: this.saveButton },
{ component: this.discardButton },
{ component: this.resetAllButton }
]).component();
}
private initializeConnectButton(): void {
this.connectToServerButton = this.modelView.modelBuilder.button().withProps({
label: loc.connectToServer,
enabled: false,
CSSStyles: { 'max-width': '125px' }
}).component();
this.disposables.push(
this.connectToServerButton!.onDidClick(async () => {
this.connectToServerButton!.enabled = false;
if (!vscode.extensions.getExtension(loc.postgresExtension)) {
const response = await vscode.window.showErrorMessage(loc.missingExtension('PostgreSQL'), loc.yes, loc.no);
if (response !== loc.yes) {
this.connectToServerButton!.enabled = true;
return;
}
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.installingExtension(loc.postgresExtension),
cancellable: false
},
async (_progress, _token): Promise<void> => {
try {
await vscode.commands.executeCommand('workbench.extensions.installExtension', loc.postgresExtension);
} catch (err) {
vscode.window.showErrorMessage(loc.extensionInstallationFailed(loc.postgresExtension));
this.connectToServerButton!.enabled = true;
throw err;
}
}
);
vscode.window.showInformationMessage(loc.extensionInstalled(loc.postgresExtension));
}
this._parametersTableLoading!.loading = true;
await this.callGetEngineSettings().finally(() => this._parametersTableLoading!.loading = false);
this.searchBox!.enabled = true;
this.resetAllButton!.enabled = true;
this.parameterContainer!.clearItems();
this.parameterContainer!.addItem(this.parametersTable);
})
);
}
private selectComponent(): void {
if (!this._postgresModel.engineSettingsLastUpdated) {
this.parameterContainer!.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.connectToPostgresDescription,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component());
this.parameterContainer!.addItem(this.connectToServerButton!, { CSSStyles: { 'max-width': '125px' } });
this.parameterContainer!.addItem(this._parametersTableLoading!);
} else {
this.searchBox!.enabled = true;
this.resetAllButton!.enabled = true;
this.parameterContainer!.addItem(this.parametersTable!);
this.refreshParametersTable();
}
}
private async callGetEngineSettings(): Promise<void> {
try {
await this._postgresModel.getEngineSettings();
} catch (error) {
if (error instanceof UserCancelledError) {
vscode.window.showWarningMessage(loc.pgConnectionRequired);
} else {
vscode.window.showErrorMessage(loc.fetchEngineSettingsFailed(this._postgresModel.info.name, error));
}
this.connectToServerButton!.enabled = true;
throw error;
}
}
private initializeSearchBox(): void {
this.searchBox = this.modelView.modelBuilder.inputBox().withProps({
readOnly: false,
enabled: false,
placeHolder: loc.searchToFilter
}).component();
this.disposables.push(
this.searchBox.onTextChanged(() => {
this.onSearchFilter();
})
);
}
@debounce(500)
private onSearchFilter(): void {
if (!this.searchBox!.value) {
this.parametersTable.setFilter(undefined);
} else {
this.filterParameters(this.searchBox!.value);
}
}
private filterParameters(search: string): void {
const filteredRowIndexes: number[] = [];
this.parametersTable.data?.forEach((row, index) => {
if (row[0].toUpperCase()?.search(search.toUpperCase()) !== -1 || row[2].toUpperCase()?.search(search.toUpperCase()) !== -1) {
filteredRowIndexes.push(index);
}
});
this.parametersTable.setFilter(filteredRowIndexes);
}
private handleOnTextChanged(component: azdata.InputBoxComponent, currentValue: string | undefined): boolean {
if (!component.valid) {
// If invalid value return false and enable discard button
this.discardButton!.enabled = true;
return false;
} else if (component.value === currentValue) {
return false;
} else {
/* If a valid value has been entered into the input box, enable save and discard buttons
so that user could choose to either edit instance or clear all inputs
return true */
this.saveButton!.enabled = true;
this.discardButton!.enabled = true;
return true;
}
}
private createParameterComponents(engineSetting: EngineSettingsModel): ParametersModel {
// Container to hold input component and information bubble
const valueContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component();
if (engineSetting.type === 'enum') {
// If type is enum, component should be drop down menu
let options = engineSetting.options?.slice(1, -1).split(',');
let values: string[] = [];
options!.forEach(option => {
values.push(option.slice(option.indexOf('"') + 1, -1));
});
let valueBox = this.modelView.modelBuilder.dropDown().withProps({
values: values,
value: engineSetting.value,
width: '150px'
}).component();
valueContainer.addItem(valueBox);
this.disposables.push(
valueBox.onValueChanged(() => {
if (engineSetting.value !== String(valueBox.value)) {
this.parameterUpdates!.set(engineSetting.parameterName!, String(valueBox.value));
this.saveButton!.enabled = true;
this.discardButton!.enabled = true;
} else if (this.parameterUpdates!.has(engineSetting.parameterName!)) {
this.parameterUpdates!.delete(engineSetting.parameterName!);
}
})
);
} else if (engineSetting.type === 'bool') {
// If type is bool, component should be checkbox to turn on or off
let valueBox = this.modelView.modelBuilder.checkBox().withProps({
label: loc.on,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
valueContainer.addItem(valueBox);
if (engineSetting.value === 'on') {
valueBox.checked = true;
} else {
valueBox.checked = false;
}
this.disposables.push(
valueBox.onChanged(() => {
if (valueBox.checked && engineSetting.value === 'off') {
this.parameterUpdates!.set(engineSetting.parameterName!, loc.on);
this.saveButton!.enabled = true;
this.discardButton!.enabled = true;
} else if (!valueBox.checked && engineSetting.value === 'on') {
this.parameterUpdates!.set(engineSetting.parameterName!, loc.off);
this.saveButton!.enabled = true;
this.discardButton!.enabled = true;
} else if (this.parameterUpdates!.has(engineSetting.parameterName!)) {
this.parameterUpdates!.delete(engineSetting.parameterName!);
}
})
);
} else if (engineSetting.type === 'string') {
// If type is string, component should be text inputbox
let valueBox = this.modelView.modelBuilder.inputBox().withProps({
required: true,
readOnly: false,
value: engineSetting.value,
width: '150px'
}).component();
valueContainer.addItem(valueBox);
this.disposables.push(
valueBox.onTextChanged(() => {
if ((this.handleOnTextChanged(valueBox, engineSetting.value))) {
this.parameterUpdates!.set(engineSetting.parameterName!, `"${valueBox.value!}"`);
} else if (this.parameterUpdates!.has(engineSetting.parameterName!)) {
this.parameterUpdates!.delete(engineSetting.parameterName!);
}
})
);
} else {
// If type is real or interger, component should be inputbox set to inputType of number. Max and min values also set.
let valueBox = this.modelView.modelBuilder.inputBox().withProps({
required: true,
readOnly: false,
min: parseInt(engineSetting.min!),
max: parseInt(engineSetting.max!),
validationErrorMessage: loc.outOfRange(engineSetting.min!, engineSetting.max!),
inputType: 'number',
value: engineSetting.value,
width: '150px'
}).component();
valueContainer.addItem(valueBox, { CSSStyles: { 'margin-right': '0px' } });
this.disposables.push(
valueBox.onTextChanged(() => {
if ((this.handleOnTextChanged(valueBox, engineSetting.value))) {
this.parameterUpdates!.set(engineSetting.parameterName!, valueBox.value!);
} else if (this.parameterUpdates!.has(engineSetting.parameterName!)) {
this.parameterUpdates!.delete(engineSetting.parameterName!);
}
})
);
// Information bubble title to show allowed values
let information = this.modelView.modelBuilder.button().withProps({
iconPath: IconPathHelper.information,
width: '15px',
height: '15px',
enabled: false,
title: loc.allowedValue(loc.rangeSetting(engineSetting.min!, engineSetting.max!))
}).component();
valueContainer.addItem(information, { CSSStyles: { 'margin-left': '5px' } });
}
// Can reset individual parameter
const resetParameterButton = this.modelView.modelBuilder.button().withProps({
iconPath: IconPathHelper.reset,
title: loc.resetToDefault,
width: '20px',
height: '20px',
enabled: true
}).component();
this.disposables.push(
resetParameterButton.onDidClick(async () => {
try {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.updatingInstance(this._postgresModel.info.name),
cancellable: false
},
async (_progress, _token): Promise<void> => {
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
try {
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{ engineSettings: engineSetting.parameterName + '=' },
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session);
} finally {
session.dispose();
}
await this._postgresModel.refresh();
}
);
vscode.window.showInformationMessage(loc.instanceUpdated(this._postgresModel.info.name));
} catch (error) {
vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._postgresModel.info.name, error));
}
})
);
let parameter: ParametersModel = {
parameterName: engineSetting.parameterName!,
valueContainer: valueContainer,
description: engineSetting.description!,
resetButton: resetParameterButton
};
return parameter;
}
private refreshParametersTable(): void {
this._parameters = this._postgresModel._engineSettings.map(engineSetting => this.createParameterComponents(engineSetting));
this.parametersTable.data = this._parameters.map(p => [p.parameterName, p.valueContainer, p.description, p.resetButton]);
}
private async handleServiceUpdated(): Promise<void> {
if (this._postgresModel.configLastUpdated && !this._postgresModel.engineSettingsLastUpdated) {
this.connectToServerButton!.enabled = true;
this._parametersTableLoading!.loading = false;
} else if (this._postgresModel.engineSettingsLastUpdated) {
await this.callGetEngineSettings();
this.discardButton!.enabled = false;
this.saveButton!.enabled = false;
}
}
}

View File

@@ -17,8 +17,8 @@ export class PostgresPropertiesPage extends DashboardPage {
private loading?: azdata.LoadingComponent;
private keyValueContainer?: KeyValueContainer;
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView);
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView, dashboard);
this.disposables.push(this._postgresModel.onConfigUpdated(
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
@@ -100,13 +100,12 @@ export class PostgresPropertiesPage extends DashboardPage {
return [
new InputKeyValue(this.modelView.modelBuilder, loc.coordinatorEndpoint, endpoint ? `postgresql://postgres@${endpoint.ip}:${endpoint.port}` : ''),
new InputKeyValue(this.modelView.modelBuilder, loc.postgresAdminUsername, 'postgres'),
new TextKeyValue(this.modelView.modelBuilder, loc.status, status ? `${status.state} (${status.readyPods} ${loc.podsReady})` : loc.unknown),
new LinkKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.controllerConfig?.metadata.namespace ?? '', () => controllerDashboard.showDashboard()),
new TextKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.controllerConfig?.metadata.namespace ?? ''),
new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.scaleConfiguration ?? ''),
new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.engineVersion ?? ''),
new InputKeyValue(this.modelView.modelBuilder, loc.subscriptionId, this._controllerModel.controllerConfig?.spec.settings.azure.subscription ?? ''),
new TextKeyValue(this.modelView.modelBuilder, loc.resourceGroup, this._controllerModel.controllerConfig?.spec.settings.azure.resourceGroup ?? ''),
new TextKeyValue(this.modelView.modelBuilder, loc.subscriptionId, this._controllerModel.controllerConfig?.spec.settings.azure.subscription ?? '')
new LinkKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.controllerConfig?.metadata.name ?? '', () => controllerDashboard.showDashboard()),
new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.scaleConfiguration ?? ''),
new TextKeyValue(this.modelView.modelBuilder, loc.status, status ? `${status.state} (${status.readyPods} ${loc.podsReady})` : loc.unknown),
new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.engineVersion ?? ''),
];
}

View File

@@ -0,0 +1,335 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles, iconSize } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
import { PostgresModel } from '../../../models/postgresModel';
export type PodHealthModel = {
condition: string,
details?: azdata.Component,
lastUpdate: string
};
export enum PodConditionType {
initialized = 'Initialized',
ready = 'Ready',
containersReady = 'ContainersReady',
podScheduled = 'PodScheduled'
}
export class PostgresResourceHealthPage extends DashboardPage {
private podSummaryContainer!: azdata.DivContainer;
private podConditionsContainer!: azdata.DivContainer;
private podConditionsLoading!: azdata.LoadingComponent;
private podConditionsTable!: azdata.DeclarativeTableComponent;
private podConditionsTableIndexes: Map<string, number[]> = new Map();
private podDropDown!: azdata.DropDownComponent;
private coordinatorPodName!: string;
private coordinatorData: PodHealthModel[] = [];
private podsData: PodHealthModel[] = [];
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _postgresModel: PostgresModel) {
super(modelView, dashboard);
this.disposables.push(
this._postgresModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleConfigUpdated())));
}
protected get title(): string {
return loc.resourceHealth;
}
protected get id(): string {
return 'postgres-resource-health';
}
protected get icon(): { dark: string; light: string; } {
return IconPathHelper.health;
}
protected get container(): azdata.Component {
const root = this.modelView.modelBuilder.divContainer().component();
const content = this.modelView.modelBuilder.divContainer().component();
root.addItem(content, { CSSStyles: { 'margin': '10px 20px 0px 20px' } });
content.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.resourceHealth,
CSSStyles: { ...cssStyles.title }
}).component());
content.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.resourceHealthDescription,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component());
this.podSummaryContainer = this.modelView.modelBuilder.divContainer().component();
this.refreshPodSummarySection();
content.addItem(this.podSummaryContainer);
// Pod Conditions
content.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.podsPresent,
CSSStyles: { ...cssStyles.title }
}).component());
content.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.podsUsedDescription,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'margin-top': '10px' }
}).component());
this.podConditionsContainer = this.modelView.modelBuilder.divContainer().component();
this.podConditionsTable = this.modelView.modelBuilder.declarativeTable().withProps({
width: '100%',
columns: [
{
displayName: loc.condition,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: '20%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
},
{
displayName: loc.details,
valueType: azdata.DeclarativeDataType.component,
isReadOnly: true,
width: '50%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: {
...cssStyles.tableRow,
'min-width': '150px'
}
},
{
displayName: loc.lastTransition,
valueType: azdata.DeclarativeDataType.string,
isReadOnly: true,
width: '30%',
headerCssStyles: cssStyles.tableHeader,
rowCssStyles: cssStyles.tableRow
}
],
data: [this.coordinatorData.map(p => [p.condition, p.details, p.lastUpdate])]
}).component();
this.podDropDown = this.modelView.modelBuilder.dropDown().withProps({ width: '150px' }).component();
this.disposables.push(
this.podDropDown.onValueChanged(() => {
this.podConditionsTable.setFilter(this.podConditionsTableIndexes.get(String(this.podDropDown.value)));
})
);
this.podConditionsContainer.addItem(this.podDropDown, { CSSStyles: { 'margin': '10px 0px 10px 0px' } });
this.podConditionsContainer.addItem(this.podConditionsTable);
this.podConditionsLoading = this.modelView.modelBuilder.loadingComponent()
.withItem(this.podConditionsContainer)
.withProperties<azdata.LoadingComponentProperties>({
loading: !this._postgresModel.configLastUpdated
}).component();
this.refreshPodConditions();
content.addItem(this.podConditionsLoading, { CSSStyles: cssStyles.text });
this.initialized = true;
return root;
}
protected get toolbarContainer(): azdata.ToolbarContainer {
// Refresh
const refreshButton = this.modelView.modelBuilder.button().withProps({
label: loc.refresh,
iconPath: IconPathHelper.refresh
}).component();
this.disposables.push(
refreshButton.onDidClick(async () => {
refreshButton.enabled = false;
try {
this.podConditionsLoading!.loading = true;
await this._postgresModel.refresh();
} catch (error) {
vscode.window.showErrorMessage(loc.refreshFailed(error));
}
finally {
refreshButton.enabled = true;
}
}));
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
{ component: refreshButton }
]).component();
}
private createPodList(): string[] {
const podStatus = this._postgresModel.config?.status.podsStatus;
let podNames: string[] = [];
podStatus?.forEach(p => {
let podHealthModels: PodHealthModel[] = [];
let indexes: number[] = [];
p.conditions.forEach(c => {
let message: string;
let imageComponent = this.modelView.modelBuilder.image().withProps({
width: iconSize,
height: iconSize,
iconHeight: '15px',
iconWidth: '15px'
}).component();
if (c.status === 'False') {
imageComponent.iconPath = IconPathHelper.fail;
message = c.message ?? c.reason ?? '';
} else {
imageComponent.iconPath = IconPathHelper.success;
if (c.type === PodConditionType.initialized) {
message = loc.podInitialized;
} else if (c.type === PodConditionType.ready) {
message = loc.podReady;
} else if (c.type === PodConditionType.containersReady) {
message = loc.containerReady;
} else if (c.type === PodConditionType.podScheduled) {
message = loc.podScheduled;
} else {
message = c.message ?? c.reason ?? '';
}
}
const conditionContainer = this.modelView.modelBuilder.flexContainer().withProps({
CSSStyles: { 'alignItems': 'center', 'height': '15px' }
}).component();
conditionContainer.addItem(imageComponent, { CSSStyles: { 'margin-right': '0px' } });
conditionContainer.addItem(this.modelView.modelBuilder.text().withProps({
value: message,
}).component());
indexes.push(this.podsData.length);
this.podsData.push({
condition: c.type,
details: conditionContainer,
lastUpdate: c.lastTransitionTime
});
});
if (p.role.toUpperCase() !== loc.coordinator.toUpperCase()) {
podNames.push(p.name);
} else {
this.coordinatorData = podHealthModels;
this.coordinatorPodName = p.name;
podNames.unshift(p.name);
}
this.podConditionsTableIndexes.set(p.name, indexes);
});
this.podConditionsTable.data = this.podsData.map(p => [p.condition, p.details, p.lastUpdate]);
return podNames;
}
private findPodIssues(): string[] {
const podStatus = this._postgresModel.config?.status.podsStatus;
let issueCount = 0;
let podIssuesDetected: string[] = [];
podStatus?.forEach(p => {
p.conditions.forEach(c => {
if (c.status === 'False') {
issueCount++;
}
});
if (issueCount > 0) {
podIssuesDetected.push(loc.numberOfIssuesDetected(p.name, issueCount));
issueCount = 0;
}
});
return podIssuesDetected;
}
private refreshPodSummarySection(): void {
let podSummaryTitle = this.modelView.modelBuilder.flexContainer().withProps({
CSSStyles: { 'alignItems': 'center', 'height': '15px', 'margin-top': '20px' }
}).component();
if (!this._postgresModel.config) {
podSummaryTitle.addItem(this.modelView.modelBuilder.loadingComponent().component(), { CSSStyles: { 'margin-right': '5px' } });
podSummaryTitle.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.loading,
CSSStyles: { ...cssStyles.title }
}).component());
this.podSummaryContainer.addItem(podSummaryTitle);
} else {
let components: azdata.Component[] = [];
let imageComponent = this.modelView.modelBuilder.image().withProps({
iconPath: IconPathHelper.success,
width: iconSize,
height: iconSize,
iconHeight: '20px',
iconWidth: '20px'
}).component();
let podIssues = this.findPodIssues();
if (podIssues.length === 0) {
imageComponent.iconPath = IconPathHelper.success;
podSummaryTitle.addItem(imageComponent, { CSSStyles: { 'margin-right': '5px' } });
podSummaryTitle.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.available,
CSSStyles: { ...cssStyles.title, 'margin-left': '0px' }
}).component());
components.push(podSummaryTitle);
components.push(this.modelView.modelBuilder.text().withProps({
value: loc.noPodIssuesDetected,
CSSStyles: { ...cssStyles.text, 'margin-top': '20px' }
}).component());
} else {
imageComponent.iconPath = IconPathHelper.fail;
podSummaryTitle.addItem(imageComponent, { CSSStyles: { 'margin-right': '5px' } });
podSummaryTitle.addItem(this.modelView.modelBuilder.text().withProps({
value: loc.issuesDetected,
CSSStyles: { ...cssStyles.title }
}).component());
components.push(podSummaryTitle);
components.push(this.modelView.modelBuilder.text().withProps({
value: loc.podIssuesDetected,
CSSStyles: { ...cssStyles.text, 'margin-top': '20px 0px 10px 0px' }
}).component());
components.push(...podIssues.map(i => {
return this.modelView.modelBuilder.text().withProps({
value: i,
CSSStyles: { ...cssStyles.text, 'margin': '0px' }
}).component();
}));
}
this.podSummaryContainer.addItems(components);
}
}
private refreshPodConditions(): void {
if (this._postgresModel.config) {
this.podConditionsTableIndexes = new Map();
this.podsData = [];
this.podDropDown.values = this.createPodList();
this.podConditionsTable.setFilter(this.podConditionsTableIndexes.get(this.coordinatorPodName!));
this.podConditionsLoading.loading = false;
}
}
private handleConfigUpdated() {
this.podSummaryContainer.clearItems();
this.refreshPodSummarySection();
this.refreshPodConditions();
}
}

View File

@@ -13,8 +13,8 @@ import { ResourceType } from 'arc';
import { PostgresModel } from '../../../models/postgresModel';
export class PostgresSupportRequestPage extends DashboardPage {
constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView);
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView, dashboard);
}
protected get title(): string {
@@ -44,6 +44,11 @@ export class PostgresSupportRequestPage extends DashboardPage {
CSSStyles: { ...cssStyles.text, 'margin-bottom': '20px' }
}).component());
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.supportRequestNote,
CSSStyles: { ...cssStyles.text, 'margin-bottom': '20px' }
}).component());
const supportRequestButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
iconPath: IconPathHelper.support,
label: loc.newSupportRequest,

View File

@@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* 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 loc from '../../../localizedConstants';
import { IconPathHelper } from '../../../constants';
import { PostgresParametersPage } from './postgresParameters';
import { PostgresModel, EngineSettingsModel } from '../../../models/postgresModel';
export class PostgresWorkerNodeParametersPage extends PostgresParametersPage {
constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, postgresModel: PostgresModel) {
super(modelView, dashboard, postgresModel);
}
protected get title(): string {
// TODO update to loc.workerNodeParameters
return loc.nodeParameters;
}
protected get id(): string {
// TODO update to 'postgres-worker-node-parameters'
return 'postgres-nodes-parameters';
}
protected get icon(): { dark: string; light: string; } {
return IconPathHelper.gearBlue;
}
protected get description(): string {
// TODO update to loc.workerNodesParametersDescription
return loc.nodeParametersDescription;
}
protected get engineSettings(): EngineSettingsModel[] {
return this._postgresModel.workerNodesEngineSettings;
}
protected async saveParameterEdits(engineSettings: string): Promise<void> {
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{ engineSettings: engineSettings },
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
this._postgresModel.controllerModel.controllerContext);
}
protected async resetAllParameters(): Promise<void> {
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{ engineSettings: `''`, replaceEngineSettings: true },
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
this._postgresModel.controllerModel.controllerContext);
}
protected async resetParameter(parameterName: string): Promise<void> {
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{ engineSettings: parameterName + '=' },
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
this._postgresModel.controllerModel.controllerContext);
}
}

View File

@@ -15,7 +15,7 @@ import { InitializingComponent } from '../components/initializingComponent';
import { AzureArcTreeDataProvider } from '../tree/azureArcTreeDataProvider';
import { getErrorMessage } from '../../common/utils';
import { RadioOptionsGroup } from '../components/radioOptionsGroup';
import { getCurrentClusterContext, getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../../common/kubeUtils';
import { getCurrentClusterContext, getDefaultKubeConfigPath, getKubeConfigClusterContexts, KubeClusterContext } from '../../common/kubeUtils';
import { FilePicker } from '../components/filePicker';
export type ConnectToControllerDialogModel = { controllerModel: ControllerModel, password: string };
@@ -25,24 +25,34 @@ abstract class ControllerDialogBase extends InitializingComponent {
protected modelBuilder!: azdata.ModelBuilder;
protected dialog: azdata.window.Dialog;
protected urlInputBox!: azdata.InputBoxComponent;
protected namespaceInputBox!: azdata.InputBoxComponent;
protected kubeConfigInputBox!: FilePicker;
protected clusterContextRadioGroup!: RadioOptionsGroup;
protected nameInputBox!: azdata.InputBoxComponent;
protected usernameInputBox!: azdata.InputBoxComponent;
protected passwordInputBox!: azdata.InputBoxComponent;
protected urlInputBox!: azdata.InputBoxComponent;
private _kubeClusters: KubeClusterContext[] = [];
protected dispose(): void {
this._toDispose.forEach(disposable => disposable.dispose());
this._toDispose.length = 0; // clear the _toDispose array
this._toDispose.length = 0;
}
protected getComponents(): (azdata.FormComponent<azdata.Component> & { layout?: azdata.FormItemLayout | undefined; })[] {
return [
{
component: this.namespaceInputBox,
title: loc.namespace,
required: true
},
{
component: this.urlInputBox,
title: loc.controllerUrl,
required: true
layout: {
info: loc.controllerUrlDescription
}
}, {
component: this.kubeConfigInputBox.component(),
title: loc.controllerKubeConfig,
@@ -54,14 +64,17 @@ abstract class ControllerDialogBase extends InitializingComponent {
}, {
component: this.nameInputBox,
title: loc.controllerName,
required: false
required: false,
layout: {
info: loc.controllerNameDescription
}
}, {
component: this.usernameInputBox,
title: loc.username,
title: loc.controllerUsername,
required: true
}, {
component: this.passwordInputBox,
title: loc.password,
title: loc.controllerPassword,
required: true
}
];
@@ -71,11 +84,14 @@ abstract class ControllerDialogBase extends InitializingComponent {
protected readonlyFields(): azdata.Component[] { return []; }
protected initializeFields(controllerInfo: ControllerInfo | undefined, password: string | undefined) {
this.namespaceInputBox = this.modelBuilder.inputBox()
.withProps({
value: controllerInfo?.namespace,
}).component();
this.urlInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
value: controllerInfo?.url,
// If we have a model then we're editing an existing connection so don't let them modify the URL
readOnly: !!controllerInfo
.withProps({
value: controllerInfo?.endpoint,
placeHolder: loc.controllerUrlPlaceholder,
}).component();
this.kubeConfigInputBox = new FilePicker(
this.modelBuilder,
@@ -83,22 +99,23 @@ abstract class ControllerDialogBase extends InitializingComponent {
(disposable) => this._toDispose.push(disposable)
);
this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
.withProps({
value: controllerInfo?.kubeConfigFilePath || getDefaultKubeConfigPath()
}).component();
this.clusterContextRadioGroup = new RadioOptionsGroup(this.modelBuilder, (disposable) => this._toDispose.push(disposable));
this.loadRadioGroup(controllerInfo?.kubeClusterContext);
this._toDispose.push(this.clusterContextRadioGroup.onRadioOptionChanged(newContext => this.updateNamespace(newContext)));
this._toDispose.push(this.kubeConfigInputBox.onTextChanged(() => this.loadRadioGroup(controllerInfo?.kubeClusterContext)));
this.nameInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
.withProps({
value: controllerInfo?.name
}).component();
this.usernameInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
.withProps({
value: controllerInfo?.username
}).component();
this.passwordInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
.withProps({
inputType: 'password',
value: password
}).component();
@@ -114,15 +131,22 @@ abstract class ControllerDialogBase extends InitializingComponent {
}
private loadRadioGroup(previousClusterContext?: string): void {
this.clusterContextRadioGroup.load(async () => {
const clusters = await getKubeConfigClusterContexts(this.kubeConfigInputBox.value!);
this.clusterContextRadioGroup.load(() => {
this._kubeClusters = getKubeConfigClusterContexts(this.kubeConfigInputBox.value!);
const currentClusterContext = getCurrentClusterContext(this._kubeClusters, previousClusterContext, false);
this.namespaceInputBox.value = currentClusterContext.namespace || this.namespaceInputBox.value;
return {
values: clusters.map(c => c.name),
defaultValue: getCurrentClusterContext(clusters, previousClusterContext, false),
values: this._kubeClusters.map(c => c.name),
defaultValue: currentClusterContext.name
};
});
}
private updateNamespace(currentContextName: string | undefined): void {
const currentContext = this._kubeClusters.find(cluster => cluster.name === currentContextName);
this.namespaceInputBox.value = currentContext?.namespace;
}
public showDialog(controllerInfo?: ControllerInfo, password: string | undefined = undefined): azdata.window.Dialog {
this.id = controllerInfo?.id ?? uuid();
this.resources = controllerInfo?.resources ?? [];
@@ -168,7 +192,8 @@ abstract class ControllerDialogBase extends InitializingComponent {
protected getControllerInfo(url: string, rememberPassword: boolean = false): ControllerInfo {
return {
id: this.id,
url: url,
endpoint: url || undefined,
namespace: this.namespaceInputBox.value!,
kubeConfigFilePath: this.kubeConfigInputBox.value!,
kubeClusterContext: this.clusterContextRadioGroup.value!,
name: this.nameInputBox.value ?? '',
@@ -183,7 +208,7 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
protected rememberPwCheckBox!: azdata.CheckBoxComponent;
protected fieldToFocusOn() {
return this.urlInputBox;
return this.namespaceInputBox;
}
protected getComponents() {
@@ -209,22 +234,25 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
}
public async validate(): Promise<boolean> {
if (!this.urlInputBox.value || !this.usernameInputBox.value || !this.passwordInputBox.value) {
if (!this.namespaceInputBox.value || !this.usernameInputBox.value || !this.passwordInputBox.value) {
return false;
}
let url = this.urlInputBox.value;
// Only support https connections
if (url.toLowerCase().startsWith('http://')) {
url = url.replace('http', 'https');
}
// Append https if they didn't type it in
if (!url.toLowerCase().startsWith('https://')) {
url = `https://${url}`;
}
// Append default port if one wasn't specified
if (!/.*:\d*$/.test(url)) {
url = `${url}:30080`;
let url = this.urlInputBox.value || '';
if (url) {
// Only support https connections
if (url.toLowerCase().startsWith('http://')) {
url = url.replace('http', 'https');
}
// Append https if they didn't type it in
if (!url.toLowerCase().startsWith('https://')) {
url = `https://${url}`;
}
// Append default port if one wasn't specified
if (!/.*:\d*$/.test(url)) {
url = `${url}:30080`;
}
}
const controllerInfo: ControllerInfo = this.getControllerInfo(url, !!this.rememberPwCheckBox.checked);
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
try {
@@ -234,7 +262,7 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
controllerModel.info.name = controllerModel.info.name || controllerModel.controllerConfig?.metadata.name || loc.defaultControllerName;
} catch (err) {
this.dialog.message = {
text: loc.connectToControllerFailed(this.urlInputBox.value, err),
text: loc.connectToControllerFailed(this.namespaceInputBox.value, err),
level: azdata.window.MessageLevel.Error
};
return false;
@@ -267,11 +295,16 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
if (!this.passwordInputBox.value) {
return false;
}
const controllerInfo: ControllerInfo = this.getControllerInfo(this.urlInputBox.value!, false);
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
const azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
try {
await azdataApi.azdata.login(
this.urlInputBox.value!,
this.usernameInputBox.value!,
{
endpoint: controllerInfo.endpoint,
namespace: controllerInfo.namespace
},
controllerInfo.username,
this.passwordInputBox.value,
{
'KUBECONFIG': this.kubeConfigInputBox.value!,
@@ -293,8 +326,6 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
return false;
}
}
const controllerInfo: ControllerInfo = this.getControllerInfo(this.urlInputBox.value!, false);
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
this.completionPromise.resolve({ controllerModel: controllerModel, password: this.passwordInputBox.value });
return true;
}

View File

@@ -10,7 +10,7 @@ import { ControllerModel } from '../../models/controllerModel';
import { ControllerTreeNode } from './controllerTreeNode';
import { TreeNode } from './treeNode';
const mementoToken = 'arcDataControllers';
const mementoToken = 'arcDataControllers.v2';
/**
* The TreeDataProvider for the Azure Arc view, which displays a list of registered

View File

@@ -44,7 +44,7 @@ export class ControllerTreeNode extends TreeNode {
} catch (err) {
vscode.window.showErrorMessage(loc.errorConnectingToController(err));
try {
await this.model.refresh(false, true);
await this.model.refresh(false);
this.updateChildren(this.model.registrations);
} catch (err) {
if (!(err instanceof UserCancelledError)) {

View File

@@ -182,10 +182,10 @@
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd"
integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==
"@microsoft/azdata-test@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@microsoft/azdata-test/-/azdata-test-1.4.0.tgz#a809187ae8a065c518e3a3e2d350883e592853bc"
integrity sha512-iscDA13/XRknRCNauP9OPsSg/ulTrMJOPFA0XMyNG1it3zY8mEJxxFJcNkWTnnEWpOUFvyksvoouzYUNy1fvrQ==
"@microsoft/azdata-test@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@microsoft/azdata-test/-/azdata-test-1.5.0.tgz#5ffa9ec6b704fea439c63d7dfa46dcfcf3236747"
integrity sha512-kaDn5geXqrhcZgxCWXSrbXdUpJi5TFmi+sIPDfmhMYJa8uecn9C2rzxn5ZbxBN5cjjYOWF318dERfe+S0CWnlA==
dependencies:
http-proxy-agent "^2.1.0"
https-proxy-agent "^2.2.4"

View File

@@ -0,0 +1,486 @@
{
"metadata": {
"kernelspec": {
"name": "python3",
"display_name": "Python 3",
"language": "python"
},
"language_info": {
"name": "python",
"version": "3.6.6",
"mimetype": "text/x-python",
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"pygments_lexer": "ipython3",
"nbconvert_exporter": "python",
"file_extension": ".py"
}
},
"nbformat_minor": 2,
"nbformat": 4,
"cells": [
{
"cell_type": "markdown",
"source": [
"![Microsoft](https://raw.githubusercontent.com/microsoft/azuredatastudio/main/extensions/resource-deployment/images/microsoft-small-logo.png)\n",
"## Deploy Azure SQL Edge to an Azure VM via IoT hub\n",
"This notebook will walk you through the end to end setup of Azure SQL Edge.\n",
"1. Create an Azure Edge VM as a virtual IoT device, you can go the \"Default Settings\" cell and adjust the vm_size variable based on your needs. Available sizes and pricing information can be found [here](https://docs.microsoft.com/azure/virtual-machines/linux/sizes).\n",
"1. Create an Azure IoT hub, you can go to the \"Default Settings\" cell and adjust value of the following variables based on your needs: iot_hub_sku and iot_hub_units. Available SKUs and pricing information can be found [here](https://azure.microsoft.com/pricing/details/iot-hub/).\n",
"1. Add the device to the IoT hub\n",
"1. Deploy SQL Edge module to the device with optional package file\n",
"1. Enable connecting to the SQL Edge instance on the device\n",
"\n",
"### Dependencies\n",
"- Azure CLI. For more information, see [Azure CLI Installation](https://docs.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest).\n",
"\n",
"<span style=\"color:red\"><font size=\"3\">Please press the \"Run all\" button to run the notebook</font></span>"
],
"metadata": {
"azdata_cell_guid": "15b8cfc7-dd7f-4db8-9a3c-2151932fe7b5"
}
},
{
"cell_type": "markdown",
"source": [
"### Check dependencies"
],
"metadata": {
"azdata_cell_guid": "f78f4ff3-d4c9-4c3e-853f-4add05061eb0"
}
},
{
"cell_type": "code",
"source": [
"import sys,os,json,html,getpass,time,ntpath,uuid\n",
"\n",
"def run_command(command:str, displayCommand:str = \"\", returnObject:bool = False):\n",
" print(\"Executing: \" + (displayCommand if displayCommand != \"\" else command))\n",
" if returnObject:\n",
" output = os.popen(command).read()\n",
" print(f'Command successfully executed')\n",
" return json.loads(''.join(output))\n",
" else:\n",
" !{command}\n",
" if _exit_code != 0:\n",
" sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n')\n",
" else:\n",
" print(f'Command successfully executed')\n",
"\n",
"run_command(command='az --version')"
],
"metadata": {
"azdata_cell_guid": "70b9744f-eb59-44e8-9b35-db590ac4651d",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Install Azure IoT extension for Azure CLI"
],
"metadata": {
"azdata_cell_guid": "a7f15c68-1725-4caa-b4f7-ddc2b4934883"
}
},
{
"cell_type": "code",
"source": [
"extensions = run_command('az extension list', returnObject=True)\r\n",
"extensions = [ext for ext in extensions if ext['name'] == 'azure-cli-iot-ext']\r\n",
"if len(extensions) > 0:\r\n",
" run_command('az extension remove --name azure-cli-iot-ext')\r\n",
"run_command('az extension add --name azure-iot')"
],
"metadata": {
"azdata_cell_guid": "55bb2f96-6f7f-4aa0-9daf-d0f7f9d9243c",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Required information"
],
"metadata": {
"azdata_cell_guid": "b5dc5586-06e8-44d9-8bc3-2861d510efe5"
}
},
{
"cell_type": "code",
"source": [
"azure_subscription_id = os.environ[\"AZDATA_NB_VAR_ASDE_SUBSCRIPTIONID\"]\n",
"azure_resource_group = os.environ[\"AZDATA_NB_VAR_ASDE_RESOURCEGROUP\"]\n",
"azure_location = os.environ[\"AZDATA_NB_VAR_ASDE_AZURE_LOCATION\"]\n",
"sa_password = os.environ[\"AZDATA_NB_VAR_SA_PASSWORD\"]\n",
"vm_admin = os.environ[\"AZDATA_NB_VAR_ASDE_VM_ADMIN\"]\n",
"vm_password = os.environ[\"AZDATA_NB_VAR_ASDE_VM_PASSWORD\"]\n",
"package_path = os.environ[\"AZDATA_NB_VAR_ASDE_PACKAGE_PATH\"]\n",
"sql_port = os.environ[\"AZDATA_NB_VAR_ASDE_SQL_PORT\"]\n",
"new_rg_flag = os.environ[\"AZDATA_NB_VAR_ASDE_NEW_RESOURCEGROUP\"]\n",
"new_rg_name = os.environ[\"AZDATA_NB_VAR_ASDE_NEW_RESOURCEGROUP_NAME\"]\n",
"\n",
"if new_rg_flag == 'true':\n",
" azure_resource_group = new_rg_name\n",
"print(f'Subscription: {azure_subscription_id}')\n",
"print(f'Resource group: {azure_resource_group}')\n",
"print(f'Location: {azure_location}')\n",
"print(f'VM admin username: {vm_admin}')\n",
"print(f'VM admin password: ******')\n",
"print(f'SQL Server port: {sql_port}')\n",
"print(f'SQL Server sa password: ******')\n",
"print(f'Package path: {package_path}')"
],
"metadata": {
"azdata_cell_guid": "dde9388b-f623-4d62-bb74-36a05f5d2ea3",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Default settings"
],
"metadata": {
"azdata_cell_guid": "2a5755eb-85a7-4237-8d87-04cdab13cf40"
}
},
{
"cell_type": "code",
"source": [
"suffix = time.strftime(\"%y%m%d%H%M%S\", time.localtime())\n",
"iot_hub_name = f'hub{suffix}'\n",
"iot_hub_sku = 'S1'\n",
"iot_hub_units = 4\n",
"iot_device_id = f'vm{suffix}'\n",
"azure_storage_account = f'sa{suffix}'\n",
"storage_account_container = 'sqldatabasepackage'\n",
"sql_lcid = '1033'\n",
"sql_collation = 'SQL_Latin1_General_CP1_CI_AS'\n",
"vm_size = 'Standard_DS1_v2'"
],
"metadata": {
"azdata_cell_guid": "19ebeaf4-94c9-4d2b-bd9f-e3c6bf7f2dda",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Login to Azure"
],
"metadata": {
"azdata_cell_guid": "84f57c09-5772-4f7a-a270-4039b8d5b081"
}
},
{
"cell_type": "code",
"source": [
"run_command('az login')"
],
"metadata": {
"azdata_cell_guid": "f9e8ddee-aefa-4951-b767-b318d941d2cd",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Set active Azure subscription"
],
"metadata": {
"azdata_cell_guid": "59249fa6-f76c-4e5d-bee7-a9ebef6f873e"
}
},
{
"cell_type": "code",
"source": [
"if azure_subscription_id != \"\":\n",
" run_command(f'az account set --subscription {azure_subscription_id}')\n",
"else:\n",
" print('Using the default Azure subscription', {azure_subscription_id})\n",
"run_command(f'az account show')"
],
"metadata": {
"azdata_cell_guid": "6e085676-2cc5-4af8-819c-fa210244e6c3",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Create resource group"
],
"metadata": {
"azdata_cell_guid": "67dacbaa-92f4-4d06-90bb-8974964852aa"
}
},
{
"cell_type": "code",
"source": [
"rg_exists = run_command(f'az group exists --name {azure_resource_group}', returnObject=True)\n",
"\n",
"if rg_exists:\n",
" print(f'resource group \\\"{azure_resource_group}\\\" already exists.')\n",
"else:\n",
" run_command(f'az group create --location {azure_location} --name {azure_resource_group}')"
],
"metadata": {
"azdata_cell_guid": "f29b439e-cf05-4c35-aa47-1482ccd653bf",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Create IoT hub"
],
"metadata": {
"azdata_cell_guid": "e37a04c3-515d-4cb7-99b2-f8bc6167510e"
}
},
{
"cell_type": "code",
"source": [
"hub_list = run_command(f'az iot hub list --resource-group {azure_resource_group}', returnObject=True)\n",
"hub_list = [hub for hub in hub_list if hub['name'] == iot_hub_name]\n",
"if len(hub_list) == 0:\n",
" run_command(f'az iot hub create --name {iot_hub_name} --resource-group {azure_resource_group} --location {azure_location} --sku {iot_hub_sku} --unit {iot_hub_units}')\n",
"else:\n",
" print(f'IoT hub \\\"{iot_hub_name}\\\" already exists')"
],
"metadata": {
"azdata_cell_guid": "f9f5e4ec-82a5-45df-a408-ddb0fb21847c",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Create storage account and storage account container, then upload the package"
],
"metadata": {
"azdata_cell_guid": "90ec2b26-0c4a-4aa4-b397-f16b09b454ea"
}
},
{
"cell_type": "code",
"source": [
"storage_account_created = False\n",
"if package_path == \"\":\n",
" print(f'Package file not provided')\n",
" blob_sas = ''\n",
"else: \n",
" package_name = ntpath.basename(package_path)\n",
" storage_accounts = run_command(f'az storage account list --resource-group {azure_resource_group} --subscription {azure_subscription_id}', returnObject=True)\n",
" storage_accounts = [storage_account for storage_account in storage_accounts if storage_account['name'] == azure_storage_account]\n",
" if len(storage_accounts) == 0:\n",
" storage_account_created = True\n",
" run_command(f'az storage account create -n {azure_storage_account} -g {azure_resource_group} -l {azure_location} --sku Standard_LRS --kind Storage')\n",
" else:\n",
" print(f'storage account \\\"{azure_storage_account}\\\" already exists.')\n",
"\n",
" storage_account_key = run_command(f'az storage account keys list --account-name {azure_storage_account} --resource-group {azure_resource_group}', returnObject=True)[0]['value']\n",
" container_exists = run_command(f'az storage container exists --name {storage_account_container} --account-key {storage_account_key} --account-name {azure_storage_account} --auth-mode key --output json', returnObject=True)['exists']\n",
" if container_exists:\n",
" print(f'storage account container \\\"{storage_account_container}\\\" already exists.')\n",
" else:\n",
" run_command(f'az storage container create --name {storage_account_container} --account-key {storage_account_key} --account-name {azure_storage_account} --auth-mode key')\n",
"\n",
" blob_exists = run_command(f'az storage blob exists --container-name {storage_account_container} --name \\\"{package_name}\\\" --account-key {storage_account_key} --account-name {azure_storage_account} --auth-mode key', returnObject=True)['exists']\n",
" if blob_exists:\n",
" print(f'blob \\\"{package_name}\\\" already exists.')\n",
" else:\n",
" run_command(f'az storage blob upload --account-name {azure_storage_account} --container-name {storage_account_container} --name {package_name} --file \\\"{package_path}\\\" --account-key {storage_account_key} --auth-mode key')\n",
" now = time.localtime()\n",
" expiry = f'{(now.tm_year + 1)}-{now.tm_mon}-{now.tm_mday}'\n",
" blob_sas = run_command(f'az storage blob generate-sas --container-name {storage_account_container} --name \\\"{package_name}\\\" --account-name {azure_storage_account} --account-key {storage_account_key} --auth-mode key --full-uri --https-only --permissions r --expiry {expiry}', returnObject=True)"
],
"metadata": {
"azdata_cell_guid": "7ab2b3ec-0832-40b3-98c0-4aa87320e7ce",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Add the Edge device to the IoT hub"
],
"metadata": {
"azdata_cell_guid": "fbc5f4ac-dfe0-4543-ace1-49b796251910"
}
},
{
"cell_type": "code",
"source": [
"device_list = run_command(f'az iot hub device-identity list --edge-enabled true --hub-name {iot_hub_name} --resource-group {azure_resource_group}', returnObject=True)\n",
"device_list = [device for device in device_list if device['deviceId'] == iot_device_id]\n",
"if len(device_list) == 0:\n",
" run_command(f'az iot hub device-identity create --device-id {iot_device_id} --hub-name {iot_hub_name} --resource-group {azure_resource_group} --edge-enabled true')\n",
"else:\n",
" print(f'Edge device \\\"{iot_device_id}\\\" already exists.')\n",
"connection_string = run_command(f'az iot hub device-identity show-connection-string --device-id {iot_device_id} --hub-name {iot_hub_name} --resource-group {azure_resource_group}', returnObject=True)\n",
"connection_string = connection_string['connectionString']"
],
"metadata": {
"azdata_cell_guid": "c183c3e3-8699-4f29-993b-07bf848336e3",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Create an Edge enabled VM as an Edge device"
],
"metadata": {
"azdata_cell_guid": "41b10249-cd40-4053-b1b0-b02f562789f7"
}
},
{
"cell_type": "code",
"source": [
"iot_deploy_result = run_command((f\"az deployment group create \"\r\n",
"f\"--resource-group {azure_resource_group} \"\r\n",
"f\"--template-uri \\\"https://aka.ms/iotedge-vm-deploy\\\" \"\r\n",
"f\"--parameters vmSize={vm_size} \"\r\n",
"f\"--parameters dnsLabelPrefix={iot_device_id} \"\r\n",
"f\"--parameters adminUsername={vm_admin} \"\r\n",
"f\"--parameters deviceConnectionString={connection_string} \"\r\n",
"f\"--parameters authenticationType=sshPublicKey \"\r\n",
"f\"--parameters adminPasswordOrKey=\\\"{vm_password}\\\"\"), returnObject=True)\r\n",
"vm_resource = [resource for resource in iot_deploy_result['properties']['dependencies'] if resource['resourceType'] == 'Microsoft.Compute/virtualMachines']\r\n",
"if len(vm_resource) != 1:\r\n",
" sys.exit('Failed to deploy the IoT Edge VM')\r\n",
"vm_name = vm_resource[0]['resourceName']\r\n",
"nsg_name = vm_name.replace('vm-','nsg-')\r\n",
"ip_address = run_command(f'az vm show -d -g {azure_resource_group} -n {vm_name} --query publicIps', returnObject=True)\r\n",
"run_command(f'az network nsg rule create --name \\\"SQL\\\" --nsg-name {nsg_name} --priority 100 --resource-group {azure_resource_group} --access Allow --description \\\"Allow SQL\\\" --destination-address-prefixes \\\"*\\\" --destination-port-ranges {sql_port} --direction Inbound --source-address-prefixes Internet --protocol Tcp')"
],
"metadata": {
"azdata_cell_guid": "c8590c65-b274-460d-9659-97e81d2fd3ea",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Deploy Azure SQL Edge to the device"
],
"metadata": {
"azdata_cell_guid": "ec46957f-0795-4c75-804d-f8a7ecb26382"
}
},
{
"cell_type": "code",
"source": [
"manifest = '{\\\"modulesContent\\\":{\\\"$edgeAgent\\\":{\\\"properties.desired\\\":{\\\"modules\\\":{\\\"AzureSQLEdge\\\":{\\\"settings\\\":{\\\"image\\\":\\\"mcr.microsoft.com/azure-sql-edge\\\",\\\"createOptions\\\":\\\"{\\\\\\\"HostConfig\\\\\\\":{\\\\\\\"CapAdd\\\\\\\":[\\\\\\\"SYS_PTRACE\\\\\\\"],\\\\\\\"Binds\\\\\\\":[\\\\\\\"sqlvolume:/sqlvolume\\\\\\\"],\\\\\\\"PortBindings\\\\\\\":{\\\\\\\"1433/tcp\\\\\\\":[{\\\\\\\"HostPort\\\\\\\":\\\\\\\"<SQL_Port>\\\\\\\"}]},\\\\\\\"Mounts\\\\\\\":[{\\\\\\\"Type\\\\\\\":\\\\\\\"volume\\\\\\\",\\\\\\\"Source\\\\\\\":\\\\\\\"sqlvolume\\\\\\\",\\\\\\\"Target\\\\\\\":\\\\\\\"/var/opt/mssql\\\\\\\"}]},\\\\\\\"User\\\\\\\":\\\\\\\"0:0\\\\\\\",\\\\\\\"Env\\\\\\\":[\\\\\\\"MSSQL_AGENT_ENABLED=TRUE\\\\\\\",\\\\\\\"ClientTransportType=AMQP_TCP_Only\\\\\\\",\\\\\\\"PlanId=asde-developer-on-iot-edge\\\\\\\"]}\\\"},\\\"type\\\":\\\"docker\\\",\\\"version\\\":\\\"1.0\\\",\\\"env\\\":{\\\"ACCEPT_EULA\\\":{\\\"value\\\":\\\"Y\\\"},\\\"SA_PASSWORD\\\":{\\\"value\\\":\\\"<Default_SQL_SA_Password>\\\"},\\\"MSSQL_LCID\\\":{\\\"value\\\":\\\"<SQL_LCID>\\\"},\\\"MSSQL_COLLATION\\\":{\\\"value\\\":\\\"<SQL_Collation>\\\"}<PACKAGE_INFO>},\\\"status\\\":\\\"running\\\",\\\"restartPolicy\\\":\\\"always\\\"}},\\\"runtime\\\":{\\\"settings\\\":{\\\"minDockerVersion\\\":\\\"v1.25\\\"},\\\"type\\\":\\\"docker\\\"},\\\"schemaVersion\\\":\\\"1.0\\\",\\\"systemModules\\\":{\\\"edgeAgent\\\":{\\\"settings\\\":{\\\"image\\\":\\\"mcr.microsoft.com/azureiotedge-agent:1.0\\\",\\\"createOptions\\\":\\\"\\\"},\\\"type\\\":\\\"docker\\\"},\\\"edgeHub\\\":{\\\"settings\\\":{\\\"image\\\":\\\"mcr.microsoft.com/azureiotedge-hub:1.0\\\",\\\"createOptions\\\":\\\"{\\\\\\\"HostConfig\\\\\\\":{\\\\\\\"PortBindings\\\\\\\":{\\\\\\\"443/tcp\\\\\\\":[{\\\\\\\"HostPort\\\\\\\":\\\\\\\"443\\\\\\\"}],\\\\\\\"5671/tcp\\\\\\\":[{\\\\\\\"HostPort\\\\\\\":\\\\\\\"5671\\\\\\\"}],\\\\\\\"8883/tcp\\\\\\\":[{\\\\\\\"HostPort\\\\\\\":\\\\\\\"8883\\\\\\\"}]}}}\\\"},\\\"type\\\":\\\"docker\\\",\\\"status\\\":\\\"running\\\",\\\"restartPolicy\\\":\\\"always\\\"}}}},\\\"$edgeHub\\\":{\\\"properties.desired\\\":{\\\"routes\\\":{},\\\"schemaVersion\\\":\\\"1.0\\\",\\\"storeAndForwardConfiguration\\\":{\\\"timeToLiveSecs\\\":7200}}},\\\"AzureSQLEdge\\\":{\\\"properties.desired\\\":{\\\"ASAJobInfo\\\":\\\"<Optional_ASA_Job_SAS_URL>\\\"}}}}'\n",
"package_info = '' if blob_sas == ''else ',\\\"MSSQL_PACKAGE\\\":{\\\"value\\\":\\\"'+blob_sas+'\\\"}'\n",
"manifest = manifest.replace('<PACKAGE_INFO>', package_info).replace('<Default_SQL_SA_Password>',sa_password).replace('<SQL_LCID>',sql_lcid).replace('<SQL_Port>',sql_port).replace('<SQL_Collation>',sql_collation)\n",
"file_name = f'{uuid.uuid4().hex}.json'\n",
"manifest_file = open(file_name, 'w')\n",
"manifest_file.write(manifest)\n",
"manifest_file.close()\n",
"run_command(f'az iot edge set-modules --device-id \\\"{iot_device_id}\\\" --hub-name \\\"{iot_hub_name}\\\" --content \\\"{file_name}\\\" --resource-group {azure_resource_group}')\n",
"os.remove(file_name)"
],
"metadata": {
"azdata_cell_guid": "81a86ff6-5a83-48be-8be7-654d152eea89",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### **Connect to Azure SQL Edge instance in Azure Data Studio**\n",
"Click the link below to connect to the Azure SQL Edge instance, it might take a couple minutes for the service to start."
],
"metadata": {
"azdata_cell_guid": "3bdfa537-a749-45c4-b219-57d296c22739"
}
},
{
"cell_type": "code",
"source": [
"from IPython.display import *\n",
"connectionParameter = '{\"serverName\":\"' + f'{ip_address},{sql_port}' + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\": \"sa\",\"password\":' + json.dumps(sa_password) + '}'\n",
"display(HTML('<br/><a href=\"command:azdata.connect?' + html.escape(connectionParameter)+'\"><font size=\"3\">Click here to connect to the Azure SQL Edge instance</font></a><br/>'))\n",
"display(HTML('<br/><span style=\"color:red\"><font size=\"2\">NOTE: The Azure SQL Edge instance password is included in this link, you may want to clear the results of this code cell before saving the notebook.</font></span>'))"
],
"metadata": {
"azdata_cell_guid": "8bc29cce-96a7-4a78-89af-5c73a6431c24",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"source": [
"if storage_account_created:\r\n",
" delete_storage_account_command = \"run_command(f'az storage account delete -n {azure_storage_account} -g {azure_resource_group} --yes')\"\r\n",
" display(HTML('<span style=\"color:red\"><font size=\"2\">NOTE: A storage account was created to host the package file, you can delete it after the database is created and populated successfully. To delete the storage account, copy the following code to a new code cell and run the cell.</font></span>'))\r\n",
" display(HTML('<span><font size=\"2\">'+delete_storage_account_command+'</font></span>'))"
],
"metadata": {
"azdata_cell_guid": "8b74ac43-a871-4d28-832d-e6da586f6d3a",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
}
]
}

View File

@@ -2,7 +2,8 @@
"metadata": {
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
"display_name": "Python 3",
"language": "python"
},
"language_info": {
"name": "python",
@@ -162,21 +163,15 @@
"cell_type": "code",
"source": [
"suffix = time.strftime(\"%y%m%d%H%M%S\", time.localtime())\n",
"network_security_group = f'nsg{suffix}'\n",
"public_ip_address_name = f'ip{suffix}'\n",
"iot_hub_name = f'hub{suffix}'\n",
"iot_hub_sku = 'S1'\n",
"iot_hub_units = 4\n",
"iot_device_id = f'vm{suffix}'\n",
"vm_size = 'Standard_DS3_v2'\n",
"vnet_name = f'net{suffix}'\n",
"subnet_name = f'subnet{suffix}'\n",
"subnet_address_prefix = '10.0.0.0/24'\n",
"vnet_address_prefix = '10.0.0.0/16'\n",
"azure_storage_account = f'sa{suffix}'\n",
"storage_account_container = 'sqldatabasepackage'\n",
"sql_lcid = '1033'\n",
"sql_collation = 'SQL_Latin1_General_CP1_CI_AS'"
"sql_collation = 'SQL_Latin1_General_CP1_CI_AS'\n",
"vm_size = 'Standard_DS1_v2'"
],
"metadata": {
"azdata_cell_guid": "19ebeaf4-94c9-4d2b-bd9f-e3c6bf7f2dda",
@@ -265,6 +260,34 @@
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Create IoT hub"
],
"metadata": {
"azdata_cell_guid": "e37a04c3-515d-4cb7-99b2-f8bc6167510e"
}
},
{
"cell_type": "code",
"source": [
"hub_list = run_command(f'az iot hub list --resource-group {azure_resource_group}', returnObject=True)\n",
"hub_list = [hub for hub in hub_list if hub['name'] == iot_hub_name]\n",
"if len(hub_list) == 0:\n",
" run_command(f'az iot hub create --name {iot_hub_name} --resource-group {azure_resource_group} --location {azure_location} --sku {iot_hub_sku} --unit {iot_hub_units}')\n",
"else:\n",
" print(f'IoT hub \\\"{iot_hub_name}\\\" already exists')"
],
"metadata": {
"azdata_cell_guid": "f9f5e4ec-82a5-45df-a408-ddb0fb21847c",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
@@ -319,25 +342,26 @@
{
"cell_type": "markdown",
"source": [
"### Create network security group"
"### Add the Edge device to the IoT hub"
],
"metadata": {
"azdata_cell_guid": "b308771b-138a-40ce-a9d3-1d15094d537b"
"azdata_cell_guid": "fbc5f4ac-dfe0-4543-ace1-49b796251910"
}
},
{
"cell_type": "code",
"source": [
"nsg_list = run_command(f'az network nsg list --resource-group {azure_resource_group}', returnObject=True)\n",
"nsg_list = [nsg for nsg in nsg_list if nsg['name'] == network_security_group]\n",
"if len(nsg_list) == 0:\n",
" run_command(f'az network nsg create --name {network_security_group} --resource-group {azure_resource_group} --location {azure_location}')\n",
" run_command(f'az network nsg rule create --name \\\"SQL\\\" --nsg-name {network_security_group} --priority 100 --resource-group {azure_resource_group} --access Allow --description \\\"Allow SQL\\\" --destination-address-prefixes \\\"*\\\" --destination-port-ranges {sql_port} --direction Inbound --source-address-prefixes Internet --protocol Tcp')\n",
"device_list = run_command(f'az iot hub device-identity list --edge-enabled true --hub-name {iot_hub_name} --resource-group {azure_resource_group}', returnObject=True)\n",
"device_list = [device for device in device_list if device['deviceId'] == iot_device_id]\n",
"if len(device_list) == 0:\n",
" run_command(f'az iot hub device-identity create --device-id {iot_device_id} --hub-name {iot_hub_name} --resource-group {azure_resource_group} --edge-enabled true')\n",
"else:\n",
" print(f'Network security group \\\"{network_security_group}\\\" already exists.')"
" print(f'Edge device \\\"{iot_device_id}\\\" already exists.')\n",
"connection_string = run_command(f'az iot hub device-identity show-connection-string --device-id {iot_device_id} --hub-name {iot_hub_name} --resource-group {azure_resource_group}', returnObject=True)\n",
"connection_string = connection_string['connectionString']"
],
"metadata": {
"azdata_cell_guid": "99cbb95c-b109-4b2e-909b-ff71a62754fb",
"azdata_cell_guid": "c183c3e3-8699-4f29-993b-07bf848336e3",
"tags": [
"hide_input"
]
@@ -357,103 +381,25 @@
{
"cell_type": "code",
"source": [
"vm_list = run_command(f'az vm list --resource-group {azure_resource_group}', returnObject=True)\n",
"vm_list = [vm for vm in vm_list if vm['name'] == iot_device_id]\n",
"if len(vm_list) == 0:\n",
" vm_image = run_command(f'az vm image list --all --location {azure_location} --offer iot_edge_vm_ubuntu --publisher microsoft_iot_edge --sku ubuntu_1604_edgeruntimeonly', returnObject=True)\n",
" image_urn = vm_image[0]['urn']\n",
" run_command(f'az vm image terms accept --urn {image_urn}')\n",
" vm_password_placeholder = '<admin_password>'\n",
" create_vm_command_template = f'az vm create --name {iot_device_id} --resource-group {azure_resource_group} --admin-username {vm_admin} --admin-password {vm_password_placeholder} --authentication-type password --image {image_urn} --location {azure_location} --nsg {network_security_group} --public-ip-address \\\"{public_ip_address_name}\\\" --public-ip-address-allocation static --public-ip-sku Standard --size {vm_size} --subnet {subnet_name} --subnet-address-prefix \\\"{subnet_address_prefix}\\\" --vnet-name {vnet_name} --vnet-address-prefix \\\"{vnet_address_prefix}\\\"'\n",
" run_command(create_vm_command_template.replace(vm_password_placeholder, vm_password), displayCommand=create_vm_command_template.replace(vm_password_placeholder, '******'))\n",
"else:\n",
" print(f'VM \\\"{iot_device_id}\\\" already exists, skipping the vm creation.')\n",
"ip_address = run_command(f'az vm show -d -g {azure_resource_group} -n {iot_device_id} --query publicIps', returnObject=True)"
"iot_deploy_result = run_command((f\"az deployment group create \"\r\n",
"f\"--resource-group {azure_resource_group} \"\r\n",
"f\"--template-uri \\\"https://aka.ms/iotedge-vm-deploy\\\" \"\r\n",
"f\"--parameters vmSize={vm_size} \"\r\n",
"f\"--parameters dnsLabelPrefix={iot_device_id} \"\r\n",
"f\"--parameters adminUsername={vm_admin} \"\r\n",
"f\"--parameters deviceConnectionString={connection_string} \"\r\n",
"f\"--parameters authenticationType=password \"\r\n",
"f\"--parameters adminPasswordOrKey=\\\"{vm_password}\\\"\"), returnObject=True)\r\n",
"vm_resource = [resource for resource in iot_deploy_result['properties']['dependencies'] if resource['resourceType'] == 'Microsoft.Compute/virtualMachines']\r\n",
"if len(vm_resource) != 1:\r\n",
" sys.exit('Failed to deploy the IoT Edge VM')\r\n",
"vm_name = vm_resource[0]['resourceName']\r\n",
"nsg_name = vm_name.replace('vm-','nsg-')\r\n",
"ip_address = run_command(f'az vm show -d -g {azure_resource_group} -n {vm_name} --query publicIps', returnObject=True)\r\n",
"run_command(f'az network nsg rule create --name \\\"SQL\\\" --nsg-name {nsg_name} --priority 100 --resource-group {azure_resource_group} --access Allow --description \\\"Allow SQL\\\" --destination-address-prefixes \\\"*\\\" --destination-port-ranges {sql_port} --direction Inbound --source-address-prefixes Internet --protocol Tcp')"
],
"metadata": {
"azdata_cell_guid": "157fc38f-cf2a-40c6-9c9e-88f45cc5c62f",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Create IoT hub"
],
"metadata": {
"azdata_cell_guid": "e37a04c3-515d-4cb7-99b2-f8bc6167510e"
}
},
{
"cell_type": "code",
"source": [
"hub_list = run_command(f'az iot hub list --resource-group {azure_resource_group}', returnObject=True)\n",
"hub_list = [hub for hub in hub_list if hub['name'] == iot_hub_name]\n",
"if len(hub_list) == 0:\n",
" run_command(f'az iot hub create --name {iot_hub_name} --resource-group {azure_resource_group} --location {azure_location} --sku {iot_hub_sku} --unit {iot_hub_units}')\n",
"else:\n",
" print(f'IoT hub \\\"{iot_hub_name}\\\" already exists')"
],
"metadata": {
"azdata_cell_guid": "f9f5e4ec-82a5-45df-a408-ddb0fb21847c",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Add the Edge device to the IoT hub"
],
"metadata": {
"azdata_cell_guid": "fbc5f4ac-dfe0-4543-ace1-49b796251910"
}
},
{
"cell_type": "code",
"source": [
"device_list = run_command(f'az iot hub device-identity list --edge-enabled true --hub-name {iot_hub_name} --resource-group {azure_resource_group}', returnObject=True)\n",
"device_list = [device for device in device_list if device['deviceId'] == iot_device_id]\n",
"if len(device_list) == 0:\n",
" run_command(f'az iot hub device-identity create --device-id {iot_device_id} --hub-name {iot_hub_name} --resource-group {azure_resource_group} --edge-enabled true')\n",
"else:\n",
" print(f'Edge device \\\"{iot_device_id}\\\" already exists.')"
],
"metadata": {
"azdata_cell_guid": "c183c3e3-8699-4f29-993b-07bf848336e3",
"tags": [
"hide_input"
]
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"### Configure Edge on the device"
],
"metadata": {
"azdata_cell_guid": "069db017-9169-499a-839b-9cd73ea7d01e"
}
},
{
"cell_type": "code",
"source": [
"connection_string = run_command(f'az iot hub device-identity show-connection-string --device-id {iot_device_id} --hub-name {iot_hub_name} --resource-group {azure_resource_group}', returnObject=True)\n",
"connection_string = connection_string['connectionString']\n",
"script = f'/etc/iotedge/configedge.sh \\'{connection_string}\\''\n",
"run_command(f'az vm run-command invoke -g {azure_resource_group} -n {iot_device_id} --command-id RunShellScript --script \\\"{script}\\\"')"
],
"metadata": {
"azdata_cell_guid": "9ec1e31a-79aa-49f4-a0e5-16f8d7c2dd21",
"azdata_cell_guid": "c8590c65-b274-460d-9659-97e81d2fd3ea",
"tags": [
"hide_input"
]

View File

@@ -2,7 +2,7 @@
"name": "asde-deployment",
"displayName": "%extension-displayName%",
"description": "%extension-description%",
"version": "0.4.1",
"version": "0.4.2",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
@@ -10,7 +10,7 @@
"aiKey": "AIF-444c3af9-8e69-4462-ab49-4191e6ad1916",
"engines": {
"vscode": "*",
"azdata": "*"
"azdata": ">=1.25.0"
},
"repository": {
"type": "git",
@@ -43,9 +43,13 @@
"displayName": "%sql-edge-remote-display-name%"
},
{
"name": "azure-create-new",
"name": "azure-create-new-password-auth",
"displayName": "%sql-edge-azure-display-name%"
},
{
"name": "azure-create-new-sshkey-auth",
"displayName": "%sql-edge-azure-sshkey-display-name%"
},
{
"name": "azure-single-device",
"displayName": "%sql-edge-azure-single-device-display-name%"
@@ -59,6 +63,7 @@
],
"providers": [
{
"name": "sql-edge_local",
"dialog": {
"notebook": "./notebooks/edge/deploy-sql-edge-local.ipynb",
"title": "%sql-edge-local-title%",
@@ -152,6 +157,7 @@
"when": "type=local"
},
{
"name": "sql-edge_remote",
"dialog": {
"notebook": "./notebooks/edge/deploy-sql-edge-remote.ipynb",
"title": "%sql-edge-remote-title%",
@@ -269,6 +275,7 @@
"when": "type=remote"
},
{
"name": "sql-edge-azure-create-new-password-auth",
"dialog": {
"notebook": "./notebooks/edge/deploy-sql-edge-azure.ipynb",
"title": "%sql-edge-azure-title%",
@@ -396,9 +403,132 @@
"version": "2.13.0"
}
],
"when": "type=azure-create-new"
"when": "type=azure-create-new-password-auth"
},
{
"name": "sql-edge-azure-create-new-sshkey-auth",
"dialog": {
"notebook": "./notebooks/edge/deploy-sql-edge-azure-sshkey.ipynb",
"title": "%sql-edge-azure-title%",
"name": "sql-edge-azure-dialog",
"tabs": [
{
"title": "",
"sections": [
{
"title": "%azure-info-section-title%",
"collapsible": true,
"fields": [
{
"subscriptionVariableName": "AZDATA_NB_VAR_ASDE_SUBSCRIPTIONID",
"resourceGroupVariableName": "AZDATA_NB_VAR_ASDE_RESOURCEGROUP",
"type": "azure_account",
"required": true,
"allowNewResourceGroup": true,
"newResourceGroupFlagVariableName": "AZDATA_NB_VAR_ASDE_NEW_RESOURCEGROUP",
"newResourceGroupNameVariableName": "AZDATA_NB_VAR_ASDE_NEW_RESOURCEGROUP_NAME"
},
{
"type": "azure_locations",
"label": "%azure_location%",
"defaultValue": "westus",
"required": true,
"locationVariableName": "AZDATA_NB_VAR_ASDE_AZURE_LOCATION",
"locations": [
"australiaeast",
"australiasoutheast",
"brazilsouth",
"canadacentral",
"canadaeast",
"centralindia",
"centralus",
"eastasia",
"eastus",
"eastus2",
"francecentral",
"japaneast",
"japanwest",
"koreacentral",
"koreasouth",
"northcentralus",
"northeurope",
"southcentralus",
"southindia",
"southeastasia",
"uksouth",
"ukwest",
"westcentralus",
"westeurope",
"westus",
"westus2"
]
},
{
"label": "%vm_admin%",
"variableName": "AZDATA_NB_VAR_ASDE_VM_ADMIN",
"type": "text",
"required": true
},
{
"label": "%vm_ssh_public_key%",
"variableName": "AZDATA_NB_VAR_ASDE_VM_PASSWORD",
"type": "text",
"required": true
}
]
},
{
"title": "%sqlserver-info-section-title%",
"collapsible": true,
"fields": [
{
"label": "%docker-sql-password-field%",
"variableName": "AZDATA_NB_VAR_SA_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_ASDE_SQL_PORT",
"type": "number",
"defaultValue": 31433,
"required": true
},
{
"label": "%package_path%",
"description": "%package_path_description%",
"variableName": "AZDATA_NB_VAR_ASDE_PACKAGE_PATH",
"type": "file_picker",
"required": false,
"filter": {
"displayName": "%package-files%",
"fileTypes": [
"zip",
"bacpac",
"dacpac"
]
}
}
]
}
]
}
]
},
"requiredTools": [
{
"name": "azure-cli",
"version": "2.13.0"
}
],
"when": "type=azure-create-new-sshkey-auth"
},
{
"name": "sql-edge_azure-single-device",
"dialog": {
"notebook": "./notebooks/edge/deploy-sql-edge-single-device.ipynb",
"title": "%sql-edge-azure-single-device-title%",
@@ -489,6 +619,7 @@
"when": "type=azure-single-device"
},
{
"name": "sql-edge_azure-multi-device",
"dialog": {
"notebook": "./notebooks/edge/deploy-sql-edge-multi-device.ipynb",
"title": "%sql-edge-azure-multi-device-title%",

View File

@@ -25,7 +25,8 @@
"edge-remote-target-field": "Name or IP address",
"edge-remote-username-field": "Username",
"edge-remote-password-field": "Password",
"sql-edge-azure-display-name": "New Azure IoT Hub and VM",
"sql-edge-azure-display-name": "New Azure IoT Hub and VM (password authentication)",
"sql-edge-azure-sshkey-display-name": "New Azure IoT Hub and VM (ssh public key authentication)",
"sql-edge-azure-title": "Deploy Azure SQL Edge to a new Azure VM via IoT hub",
"azure_subscription_id": "Subscription id",
"azure_resource_group": "Resource group",
@@ -48,5 +49,6 @@
"sql-edge-azure-multi-device-display-name": "Multiple devices of an Azure IoT Hub",
"sql-edge-azure-multi-device-title": "Deploy Azure SQL Edge to multiple Azure IoT devices",
"device-target-condition": "Target condition",
"device-target-condition-learn-more": "Learn more about target condition"
"device-target-condition-learn-more": "Learn more about target condition",
"vm_ssh_public_key": "SSH public key"
}

View File

@@ -2,7 +2,7 @@
"name": "azdata",
"displayName": "%azdata.displayName%",
"description": "%azdata.description%",
"version": "0.5.1",
"version": "0.6.2",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
@@ -107,7 +107,12 @@
"when": "azdata.found"
}
]
}
},
"resourceDeploymentOptionsSources": [
{
"id": "arc.controller.config.profiles"
}
]
},
"dependencies": {
"request": "^2.88.2",

View File

@@ -5,13 +5,26 @@
import * as azdataExt from 'azdata-ext';
import * as vscode from 'vscode';
import { IAzdataTool, isEulaAccepted, promptForEula } from './azdata';
import { IAzdataTool, isEulaAccepted, MIN_AZDATA_VERSION, promptForEula } from './azdata';
import Logger from './common/logger';
import { NoAzdataError } from './common/utils';
import * as constants from './constants';
import * as loc from './localizedConstants';
import { AzdataToolService } from './services/azdataToolService';
/**
* Validates that :
* - Azdata is installed
* - The Azdata version is >= the minimum required version
* - The Azdata CLI has been accepted
* @param azdata The azdata tool to check
* @param eulaAccepted Whether the Azdata CLI EULA has been accepted
*/
async function validateAzdata(azdata: IAzdataTool | undefined, eulaAccepted: boolean): Promise<void> {
throwIfNoAzdataOrEulaNotAccepted(azdata, eulaAccepted);
await throwIfRequiredVersionMissing(azdata);
}
export function throwIfNoAzdataOrEulaNotAccepted(azdata: IAzdataTool | undefined, eulaAccepted: boolean): asserts azdata {
throwIfNoAzdata(azdata);
if (!eulaAccepted) {
@@ -20,6 +33,13 @@ export function throwIfNoAzdataOrEulaNotAccepted(azdata: IAzdataTool | undefined
}
}
export async function throwIfRequiredVersionMissing(azdata: IAzdataTool): Promise<void> {
const currentVersion = await azdata.getSemVersion();
if (currentVersion.compare(MIN_AZDATA_VERSION) < 0) {
throw new Error(loc.missingRequiredVersion(MIN_AZDATA_VERSION.raw));
}
}
export function throwIfNoAzdata(localAzdata: IAzdataTool | undefined): asserts localAzdata {
if (!localAzdata) {
Logger.log(loc.noAzdata);
@@ -55,47 +75,47 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
profileName?: string,
storageClass?: string,
additionalEnvVars?: azdataExt.AdditionalEnvVars,
session?: azdataExt.AzdataSession) => {
azdataContext?: string) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass, additionalEnvVars, session);
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass, additionalEnvVars, azdataContext);
},
endpoint: {
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.dc.endpoint.list(additionalEnvVars, session);
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.dc.endpoint.list(additionalEnvVars, azdataContext);
}
},
config: {
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.dc.config.list(additionalEnvVars, session);
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.dc.config.list(additionalEnvVars, azdataContext);
},
show: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
show: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.dc.config.show(additionalEnvVars, session);
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.dc.config.show(additionalEnvVars, azdataContext);
}
}
},
postgres: {
server: {
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.delete(name, additionalEnvVars, session);
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.postgres.server.delete(name, additionalEnvVars, azdataContext);
},
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.list(additionalEnvVars, session);
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.postgres.server.list(additionalEnvVars, azdataContext);
},
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.show(name, additionalEnvVars, session);
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.postgres.server.show(name, additionalEnvVars, azdataContext);
},
edit: async (
name: string,
@@ -112,31 +132,30 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
replaceEngineSettings?: boolean;
workers?: number;
},
engineVersion?: string,
additionalEnvVars?: azdataExt.AdditionalEnvVars,
session?: azdataExt.AzdataSession) => {
azdataContext?: string) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.edit(name, args, engineVersion, additionalEnvVars, session);
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.postgres.server.edit(name, args, additionalEnvVars, azdataContext);
}
}
},
sql: {
mi: {
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.delete(name, additionalEnvVars, session);
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.sql.mi.delete(name, additionalEnvVars, azdataContext);
},
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.list(additionalEnvVars, session);
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.sql.mi.list(additionalEnvVars, azdataContext);
},
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.show(name, additionalEnvVars, session);
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.sql.mi.show(name, additionalEnvVars, azdataContext);
},
edit: async (
name: string,
@@ -148,11 +167,11 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
noWait?: boolean;
},
additionalEnvVars?: azdataExt.AdditionalEnvVars,
session?: azdataExt.AzdataSession
azdataContext?: string
) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.edit(name, args, additionalEnvVars, session);
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.sql.mi.edit(name, args, additionalEnvVars, azdataContext);
}
}
}
@@ -162,13 +181,9 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
throwIfNoAzdata(azdataToolService.localAzdata);
return azdataToolService.localAzdata.getPath();
},
login: async (endpoint: string, username: string, password: string, additionalEnvVars?: azdataExt.AdditionalEnvVars) => {
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.login(endpoint, username, password, additionalEnvVars);
},
acquireSession: async (endpoint: string, username: string, password: string, additionEnvVars?: azdataExt.AdditionalEnvVars) => {
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata?.acquireSession(endpoint, username, password, additionEnvVars);
login: async (endpointOrNamespace: azdataExt.EndpointOrNamespace, username: string, password: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.login(endpointOrNamespace, username, password, additionalEnvVars, azdataContext);
},
getSemVersion: async () => {
await localAzdataDiscovered;

View File

@@ -13,11 +13,15 @@ import { getPlatformDownloadLink, getPlatformReleaseVersion } from './azdataRele
import { executeCommand, executeSudoCommand, ExitCodeError, ProcessOutput } from './common/childProcess';
import { HttpClient } from './common/httpClient';
import Logger from './common/logger';
import { Deferred } from './common/promise';
import { getErrorMessage, NoAzdataError, searchForCmd } from './common/utils';
import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataInstallKey, azdataUpdateKey, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants';
import * as loc from './localizedConstants';
/**
* The minimum required azdata CLI version for this extension to function properly
*/
export const MIN_AZDATA_VERSION = new SemVer('20.3.2');
export const enum AzdataDeployOption {
dontPrompt = 'dontPrompt',
prompt = 'prompt'
@@ -32,20 +36,7 @@ export interface IAzdataTool extends azdataExt.IAzdataApi {
* @param args The args to pass to azdata
* @param parseResult A function used to parse out the raw result into the desired shape
*/
executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<R>>
}
class AzdataSession implements azdataExt.AzdataSession {
private _session = new Deferred<void>();
public sessionEnded(): Promise<void> {
return this._session.promise;
}
public dispose(): void {
this._session.resolve();
}
executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<R>>
}
/**
@@ -54,9 +45,6 @@ class AzdataSession implements azdataExt.AzdataSession {
export class AzdataTool implements azdataExt.IAzdataApi {
private _semVersion: SemVer;
private _currentSession: azdataExt.AzdataSession | undefined = undefined;
private _currentlyExecutingCommands: Deferred<void>[] = [];
private _queuedCommands: { deferred: Deferred<void>, session?: azdataExt.AzdataSession }[] = [];
constructor(private _path: string, version: string) {
this._semVersion = new SemVer(version);
@@ -90,7 +78,7 @@ export class AzdataTool implements azdataExt.IAzdataApi {
profileName?: string,
storageClass?: string,
additionalEnvVars?: azdataExt.AdditionalEnvVars,
session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
const args = ['arc', 'dc', 'create',
'--namespace', namespace,
'--name', name,
@@ -104,32 +92,32 @@ export class AzdataTool implements azdataExt.IAzdataApi {
if (storageClass) {
args.push('--storage-class', storageClass);
}
return this.executeCommand<void>(args, additionalEnvVars, session);
return this.executeCommand<void>(args, additionalEnvVars, azdataContext);
},
endpoint: {
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> => {
return this.executeCommand<azdataExt.DcEndpointListResult[]>(['arc', 'dc', 'endpoint', 'list'], additionalEnvVars, session);
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> => {
return this.executeCommand<azdataExt.DcEndpointListResult[]>(['arc', 'dc', 'endpoint', 'list'], additionalEnvVars, azdataContext);
}
},
config: {
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> => {
return this.executeCommand<azdataExt.DcConfigListResult[]>(['arc', 'dc', 'config', 'list'], additionalEnvVars, session);
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> => {
return this.executeCommand<azdataExt.DcConfigListResult[]>(['arc', 'dc', 'config', 'list'], additionalEnvVars, azdataContext);
},
show: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> => {
return this.executeCommand<azdataExt.DcConfigShowResult>(['arc', 'dc', 'config', 'show'], additionalEnvVars, session);
show: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> => {
return this.executeCommand<azdataExt.DcConfigShowResult>(['arc', 'dc', 'config', 'show'], additionalEnvVars, azdataContext);
}
}
},
postgres: {
server: {
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
return this.executeCommand<void>(['arc', 'postgres', 'server', 'delete', '-n', name, '--force'], additionalEnvVars, session);
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
return this.executeCommand<void>(['arc', 'postgres', 'server', 'delete', '-n', name, '--force'], additionalEnvVars, azdataContext);
},
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> => {
return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list'], additionalEnvVars, session);
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> => {
return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list'], additionalEnvVars, azdataContext);
},
show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> => {
return this.executeCommand<azdataExt.PostgresServerShowResult>(['arc', 'postgres', 'server', 'show', '-n', name], additionalEnvVars, session);
show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> => {
return this.executeCommand<azdataExt.PostgresServerShowResult>(['arc', 'postgres', 'server', 'show', '-n', name], additionalEnvVars, azdataContext);
},
edit: (
name: string,
@@ -146,9 +134,8 @@ export class AzdataTool implements azdataExt.IAzdataApi {
replaceEngineSettings?: boolean,
workers?: number
},
engineVersion?: string,
additionalEnvVars?: azdataExt.AdditionalEnvVars,
session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name];
if (args.adminPassword) { argsArray.push('--admin-password'); }
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
@@ -161,21 +148,20 @@ export class AzdataTool implements azdataExt.IAzdataApi {
if (args.port) { argsArray.push('--port', args.port.toString()); }
if (args.replaceEngineSettings) { argsArray.push('--replace-engine-settings'); }
if (args.workers) { argsArray.push('--workers', args.workers.toString()); }
if (engineVersion) { argsArray.push('--engine-version', engineVersion); }
return this.executeCommand<void>(argsArray, additionalEnvVars, session);
return this.executeCommand<void>(argsArray, additionalEnvVars, azdataContext);
}
}
},
sql: {
mi: {
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
return this.executeCommand<void>(['arc', 'sql', 'mi', 'delete', '-n', name], additionalEnvVars, session);
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
return this.executeCommand<void>(['arc', 'sql', 'mi', 'delete', '-n', name], additionalEnvVars, azdataContext);
},
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> => {
return this.executeCommand<azdataExt.SqlMiListResult[]>(['arc', 'sql', 'mi', 'list'], additionalEnvVars, session);
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> => {
return this.executeCommand<azdataExt.SqlMiListResult[]>(['arc', 'sql', 'mi', 'list'], additionalEnvVars, azdataContext);
},
show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> => {
return this.executeCommand<azdataExt.SqlMiShowResult>(['arc', 'sql', 'mi', 'show', '-n', name], additionalEnvVars, session);
show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> => {
return this.executeCommand<azdataExt.SqlMiShowResult>(['arc', 'sql', 'mi', 'show', '-n', name], additionalEnvVars, azdataContext);
},
edit: (
name: string,
@@ -186,8 +172,7 @@ export class AzdataTool implements azdataExt.IAzdataApi {
memoryRequest?: string,
noWait?: boolean,
},
additionalEnvVars?: azdataExt.AdditionalEnvVars,
session?: azdataExt.AzdataSession
additionalEnvVars?: azdataExt.AdditionalEnvVars
): Promise<azdataExt.AzdataOutput<void>> => {
const argsArray = ['arc', 'sql', 'mi', 'edit', '-n', name];
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
@@ -195,59 +180,22 @@ export class AzdataTool implements azdataExt.IAzdataApi {
if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); }
if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
if (args.noWait) { argsArray.push('--no-wait'); }
return this.executeCommand<void>(argsArray, additionalEnvVars, session);
return this.executeCommand<void>(argsArray, additionalEnvVars);
}
}
}
};
public async login(endpoint: string, username: string, password: string, additionalEnvVars: azdataExt.AdditionalEnvVars = {}): Promise<azdataExt.AzdataOutput<void>> {
// Since login changes the context we want to wait until all currently executing commands are finished before this is executed
while (this._currentlyExecutingCommands.length > 0) {
await this._currentlyExecutingCommands[0];
}
// Logins need to be done outside the session aware logic so call impl directly
return this.executeCommandImpl<void>(['login', '-e', endpoint, '-u', username], Object.assign({}, additionalEnvVars, { 'AZDATA_PASSWORD': password }));
}
public async acquireSession(endpoint: string, username: string, password: string, additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataSession> {
const session = new AzdataSession();
session.sessionEnded().then(async () => {
// Wait for all commands running for this session to end
while (this._currentlyExecutingCommands.length > 0) {
await this._currentlyExecutingCommands[0].promise;
}
this._currentSession = undefined;
// Start our next command now that we're all done with this session
// TODO: Should we check if the command has a session that hasn't started? That should never happen..
// TODO: Look into kicking off multiple commands
this._queuedCommands.shift()?.deferred.resolve();
});
// We're not in a session or waiting on anything so just set the current session right now
if (!this._currentSession && this._queuedCommands.length === 0) {
this._currentSession = session;
public async login(endpointOrNamespace: azdataExt.EndpointOrNamespace, username: string, password: string, additionalEnvVars: azdataExt.AdditionalEnvVars = {}, azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> {
const args = ['login', '-u', username];
if (endpointOrNamespace.endpoint) {
args.push('-e', endpointOrNamespace.endpoint);
} else if (endpointOrNamespace.namespace) {
args.push('--namespace', endpointOrNamespace.namespace);
} else {
// We're in a session or another command is executing so add this to the end of the queued commands and wait our turn
const deferred = new Deferred<void>();
deferred.promise.then(() => {
this._currentSession = session;
// We've started a new session so look at all our queued commands and start
// the ones for this session now.
this._queuedCommands = this._queuedCommands.filter(c => {
if (c.session === this._currentSession) {
c.deferred.resolve();
return false;
}
return true;
});
});
this._queuedCommands.push({ deferred, session: undefined });
await deferred.promise;
throw new Error(loc.endpointOrNamespaceRequired);
}
await this.login(endpoint, username, password, additionalEnvVars);
return session;
return this.executeCommand<void>(args, Object.assign({}, additionalEnvVars, { 'AZDATA_PASSWORD': password }), azdataContext);
}
/**
@@ -265,34 +213,16 @@ export class AzdataTool implements azdataExt.IAzdataApi {
};
}
public async executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<R>> {
if (this._currentSession && this._currentSession !== session) {
const deferred = new Deferred<void>();
this._queuedCommands.push({ deferred, session: session });
await deferred.promise;
}
const executingDeferred = new Deferred<void>();
this._currentlyExecutingCommands.push(executingDeferred);
try {
return await this.executeCommandImpl<R>(args, additionalEnvVars);
}
finally {
this._currentlyExecutingCommands = this._currentlyExecutingCommands.filter(c => c !== executingDeferred);
executingDeferred.resolve();
// If there isn't an active session and we still have queued commands then we have to manually kick off the next one
if (this._queuedCommands.length > 0 && !this._currentSession) {
this._queuedCommands.shift()?.deferred.resolve();
}
}
}
/**
* Executes the specified azdata command. This is NOT session-aware so should only be used for calls that don't care about a session
* Executes the specified azdata command.
* @param args The args to pass to azdata
* @param additionalEnvVars Additional environment variables to set for this execution
*/
private async executeCommandImpl<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<R>> {
public async executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<R>> {
try {
if (azdataContext) {
args = args.concat('--controller-context', azdataContext);
}
const output = JSON.parse((await executeAzdataCommand(`"${this._path}"`, args.concat(['--output', 'json']), additionalEnvVars)).stdout);
return {
logs: <string[]>output.log,
@@ -442,8 +372,22 @@ export async function checkAndInstallAzdata(userRequested: boolean = false): Pro
export async function checkAndUpdateAzdata(currentAzdata?: IAzdataTool, userRequested: boolean = false): Promise<boolean> {
if (currentAzdata !== undefined) {
const newSemVersion = await discoverLatestAvailableAzdataVersion();
if (newSemVersion.compare(await currentAzdata.getSemVersion()) === 1) {
Logger.log(loc.foundAzdataVersionToUpdateTo(newSemVersion.raw, (await currentAzdata.getSemVersion()).raw));
const currentSemVersion = await currentAzdata.getSemVersion();
Logger.log(loc.foundAzdataVersionToUpdateTo(newSemVersion.raw, currentSemVersion.raw));
if (MIN_AZDATA_VERSION.compare(currentSemVersion) === 1) {
if (newSemVersion.compare(MIN_AZDATA_VERSION) >= 0) {
return await promptToUpdateAzdata(newSemVersion.raw, userRequested, true);
} else {
// This should never happen - it means that the currently available version to download
// is < the version we require. If this was to happen it'd imply something is wrong with
// the version JSON or the minimum required version.
// Regardless, there's nothing we can do and so we just bail out at this point and tell the user
// they have to install it manually (hopefully it's available and wasn't a publishing mistake)
vscode.window.showInformationMessage(loc.requiredVersionNotAvailable(MIN_AZDATA_VERSION.raw, newSemVersion.raw));
Logger.log(loc.requiredVersionNotAvailable(newSemVersion.raw, currentSemVersion.raw));
}
}
else if (newSemVersion.compare(currentSemVersion) === 1) {
return await promptToUpdateAzdata(newSemVersion.raw, userRequested);
} else {
Logger.log(loc.currentlyInstalledVersionIsLatest((await currentAzdata.getSemVersion()).raw));
@@ -504,39 +448,65 @@ async function promptToInstallAzdata(userRequested: boolean = false): Promise<bo
* @param newVersion - provides the new version that the user will be prompted to update to
* @param userRequested - if true this operation was requested in response to a user issued command, if false it was issued at startup by system
* returns true if update was done and false otherwise.
* @param required - Whether this update is required. If true then we will always show the prompt and warn the user if they decline it
*/
async function promptToUpdateAzdata(newVersion: string, userRequested: boolean = false): Promise<boolean> {
let response: string | undefined = loc.yes;
const config = <AzdataDeployOption>getConfig(azdataUpdateKey);
if (userRequested) {
Logger.show();
Logger.log(loc.userRequestedUpdate);
}
if (config === AzdataDeployOption.dontPrompt && !userRequested) {
Logger.log(loc.skipUpdate(config));
return false;
}
const responses = userRequested
? [loc.yes, loc.no]
: [loc.yes, loc.askLater, loc.doNotAskAgain];
if (config === AzdataDeployOption.prompt) {
Logger.log(loc.promptForAzdataUpdateLog(newVersion));
response = await vscode.window.showInformationMessage(loc.promptForAzdataUpdate(newVersion), ...responses);
async function promptToUpdateAzdata(newVersion: string, userRequested: boolean = false, required = false): Promise<boolean> {
if (required) {
let response: string | undefined = loc.yes;
const responses = [loc.yes, loc.no];
Logger.log(loc.promptForRequiredAzdataUpdateLog(MIN_AZDATA_VERSION.raw, newVersion));
response = await vscode.window.showInformationMessage(loc.promptForRequiredAzdataUpdate(MIN_AZDATA_VERSION.raw, newVersion), ...responses);
Logger.log(loc.userResponseToUpdatePrompt(response));
}
if (response === loc.doNotAskAgain) {
await setConfig(azdataUpdateKey, AzdataDeployOption.dontPrompt);
} else if (response === loc.yes) {
try {
await updateAzdata();
vscode.window.showInformationMessage(loc.azdataUpdated(newVersion));
Logger.log(loc.azdataUpdated(newVersion));
return true;
} catch (err) {
// Windows: 1602 is User cancelling installation/update - not unexpected so don't display
if (!(err instanceof ExitCodeError) || err.code !== 1602) {
vscode.window.showWarningMessage(loc.updateError(err));
Logger.log(loc.updateError(err));
if (response === loc.yes) {
try {
await updateAzdata();
vscode.window.showInformationMessage(loc.azdataUpdated(newVersion));
Logger.log(loc.azdataUpdated(newVersion));
return true;
} catch (err) {
// Windows: 1602 is User cancelling installation/update - not unexpected so don't display
if (!(err instanceof ExitCodeError) || err.code !== 1602) {
vscode.window.showWarningMessage(loc.updateError(err));
Logger.log(loc.updateError(err));
}
}
} else {
vscode.window.showWarningMessage(loc.missingRequiredVersion(MIN_AZDATA_VERSION.raw));
}
} else {
let response: string | undefined = loc.yes;
const config = <AzdataDeployOption>getConfig(azdataUpdateKey);
if (userRequested) {
Logger.show();
Logger.log(loc.userRequestedUpdate);
}
if (config === AzdataDeployOption.dontPrompt && !userRequested) {
Logger.log(loc.skipUpdate(config));
return false;
}
const responses = userRequested
? [loc.yes, loc.no]
: [loc.yes, loc.askLater, loc.doNotAskAgain];
if (config === AzdataDeployOption.prompt) {
Logger.log(loc.promptForAzdataUpdateLog(newVersion));
response = await vscode.window.showInformationMessage(loc.promptForAzdataUpdate(newVersion), ...responses);
Logger.log(loc.userResponseToUpdatePrompt(response));
}
if (response === loc.doNotAskAgain) {
await setConfig(azdataUpdateKey, AzdataDeployOption.dontPrompt);
} else if (response === loc.yes) {
try {
await updateAzdata();
vscode.window.showInformationMessage(loc.azdataUpdated(newVersion));
Logger.log(loc.azdataUpdated(newVersion));
return true;
} catch (err) {
// Windows: 1602 is User cancelling installation/update - not unexpected so don't display
if (!(err instanceof ExitCodeError) || err.code !== 1602) {
vscode.window.showWarningMessage(loc.updateError(err));
Logger.log(loc.updateError(err));
}
}
}
}

View File

@@ -6,13 +6,13 @@
/**
* Deferred promise
*/
export class Deferred<T> {
export class Deferred<T = void> {
promise: Promise<T>;
resolve!: (value?: T | PromiseLike<T>) => void;
resolve!: (value: T | PromiseLike<T>) => void;
reject!: (reason?: any) => void;
constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this.resolve = <any>resolve;
this.resolve = resolve;
this.reject = reject;
});
}

View File

@@ -38,8 +38,11 @@ export const promptLog = (logEntry: string) => localize('azdata.promptLog', "Pro
export const promptForAzdataInstall = localize('azdata.couldNotFindAzdataWithPrompt', "Could not find Azure Data CLI, install it now? If not then some features will not be able to function.");
export const promptForAzdataInstallLog = promptLog(promptForAzdataInstall);
export const promptForAzdataUpdate = (version: string): string => localize('azdata.promptForAzdataUpdate', "A new version of Azure Data CLI ( {0} ) is available, do you wish to update to it now?", version);
export const promptForRequiredAzdataUpdate = (requiredVersion: string, latestVersion: string): string => localize('azdata.promptForRequiredAzdataUpdate', "This extension requires Azure Data CLI >= {0} to be installed, do you wish to update to the latest version ({1}) now? If you do not then some functionality may not work.", requiredVersion, latestVersion);
export const requiredVersionNotAvailable = (requiredVersion: string, currentVersion: string): string => localize('azdata.requiredVersionNotAvailable', "This extension requires Azure Data CLI >= {0} to be installed, but the current version available is only {1}. Install the correct version manually from [here](https://docs.microsoft.com/sql/azdata/install/deploy-install-azdata) and then restart Azure Data Studio.", requiredVersion, currentVersion);
export const promptForAzdataUpdateLog = (version: string): string => promptLog(promptForAzdataUpdate(version));
export const promptForRequiredAzdataUpdateLog = (requiredVersion: string, latestVersion: string): string => promptLog(promptForRequiredAzdataUpdate(requiredVersion, latestVersion));
export const missingRequiredVersion = (requiredVersion: string): string => localize('azdata.missingRequiredVersion', "Azure Data CLI >= {0} is required for this extension to function, some features may not work correctly until that version or higher is installed.", requiredVersion);
export const downloadError = localize('azdata.downloadError', "Error while downloading");
export const installError = (err: any): string => localize('azdata.installError', "Error installing Azure Data CLI: {0}", err.message ?? err);
export const updateError = (err: any): string => localize('azdata.updateError', "Error updating Azure Data CLI: {0}", err.message ?? err);
@@ -66,3 +69,4 @@ export const promptForEula = (privacyStatementUrl: string, eulaUrl: string) => l
export const promptForEulaLog = (privacyStatementUrl: string, eulaUrl: string) => promptLog(promptForEula(privacyStatementUrl, eulaUrl));
export const userResponseToEulaPrompt = (response: string | undefined) => localize('azdata.promptForEulaResponse', "User response to EULA prompt: {0}", response);
export const eulaAcceptedStateOnStartup = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateOnStartup', "'EULA Accepted' state on startup: {0}", eulaAccepted);
export const endpointOrNamespaceRequired = localize('azdata.endpointOrNamespaceRequired', "Either an endpoint or a namespace must be specified");

View File

@@ -51,7 +51,7 @@ describe('api', function (): void {
it('succeed when azdata present and EULA accepted', async function (): Promise<void> {
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
mementoMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => true);
const azdataTool = new AzdataTool('', '1.0.0');
const azdataTool = new AzdataTool('', '99.0.0');
const azdataToolService = new AzdataToolService();
azdataToolService.localAzdata = azdataTool;
// Not using a mock here because it'll hang when resolving mocked objects
@@ -60,7 +60,7 @@ describe('api', function (): void {
sinon.stub(childProcess, 'executeCommand').callsFake(async (_command, args) => {
// Version needs to be valid so it can be parsed correctly
if (args[0] === '--version') {
return { stdout: `1.0.0`, stderr: '' };
return { stdout: `99.0.0`, stderr: '' };
}
console.log(args[0]);
return { stdout: `{ }`, stderr: '' };
@@ -96,15 +96,8 @@ describe('api', function (): void {
async function assertApiCalls(api: azdataExt.IExtension, assertCallback: (promise: Promise<any>, message: string) => Promise<void>): Promise<void> {
await assertCallback(api.azdata.getPath(), 'getPath');
await assertCallback(api.azdata.getSemVersion(), 'getSemVersion');
await assertCallback(api.azdata.login('', '', ''), 'login');
await assertCallback((async () => {
let session: azdataExt.AzdataSession | undefined;
try {
session = await api.azdata.acquireSession('', '', '');
} finally {
session?.dispose();
}
})(), 'acquireSession');
await assertCallback(api.azdata.login({ endpoint: 'https://127.0.0.1' }, '', ''), 'login');
await assertCallback(api.azdata.login({ namespace: 'namespace' }, '', ''), 'login');
await assertCallback(api.azdata.version(), 'version');
await assertCallback(api.azdata.arc.dc.create('', '', '', '', '', ''), 'arc dc create');
@@ -117,7 +110,7 @@ describe('api', function (): void {
await assertCallback(api.azdata.arc.sql.mi.list(), 'arc sql mi list');
await assertCallback(api.azdata.arc.sql.mi.delete(''), 'arc sql mi delete');
await assertCallback(api.azdata.arc.sql.mi.show(''), 'arc sql mi show');
await assertCallback(api.azdata.arc.sql.mi.edit('', { }), 'arc sql mi edit');
await assertCallback(api.azdata.arc.sql.mi.edit('', {}), 'arc sql mi edit');
await assertCallback(api.azdata.arc.postgres.server.list(), 'arc sql postgres server list');
await assertCallback(api.azdata.arc.postgres.server.delete(''), 'arc sql postgres server delete');
await assertCallback(api.azdata.arc.postgres.server.show(''), 'arc sql postgres server show');

View File

@@ -3,7 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdataExt from 'azdata-ext';
import * as should from 'should';
import * as sinon from 'sinon';
import * as vscode from 'vscode';
@@ -17,9 +16,8 @@ import * as fs from 'fs';
import { AzdataReleaseInfo } from '../azdataReleaseInfo';
import * as TypeMoq from 'typemoq';
import { eulaAccepted } from '../constants';
import { sleep } from './testUtils';
const oldAzdataMock = new azdata.AzdataTool('/path/to/azdata', '0.0.0');
const oldAzdataMock = new azdata.AzdataTool('/path/to/azdata', azdata.MIN_AZDATA_VERSION.raw);
const currentAzdataMock = new azdata.AzdataTool('/path/to/azdata', '9999.999.999');
/**
@@ -222,120 +220,10 @@ describe('azdata', function () {
const endpoint = 'myEndpoint';
const username = 'myUsername';
const password = 'myPassword';
await azdataTool.login(endpoint, username, password);
await azdataTool.login({ endpoint: endpoint }, username, password);
verifyExecuteCommandCalledWithArgs(['login', endpoint, username]);
});
describe('acquireSession', function (): void {
it('calls login', async function (): Promise<void> {
const endpoint = 'myEndpoint';
const username = 'myUsername';
const password = 'myPassword';
const session = await azdataTool.acquireSession(endpoint, username, password);
session.dispose();
verifyExecuteCommandCalledWithArgs(['login', endpoint, username]);
});
it('command executed under current session completes', async function (): Promise<void> {
const session = await azdataTool.acquireSession('', '', '');
try {
await azdataTool.arc.dc.config.show(undefined, session);
} finally {
session.dispose();
}
verifyExecuteCommandCalledWithArgs(['login'], 0);
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
});
it('multiple commands executed under current session completes', async function (): Promise<void> {
const session = await azdataTool.acquireSession('', '', '');
try {
// Kick off multiple commands at the same time and then ensure that they both complete
await Promise.all([
azdataTool.arc.dc.config.show(undefined, session),
azdataTool.arc.sql.mi.list(undefined, session)
]);
} finally {
session.dispose();
}
verifyExecuteCommandCalledWithArgs(['login'], 0);
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list'], 2);
});
it('command executed without session context is queued up until session is closed', async function (): Promise<void> {
const session = await azdataTool.acquireSession('', '', '');
let nonSessionCommand: Promise<any> | undefined = undefined;
try {
// Start one command in the current session
await azdataTool.arc.dc.config.show(undefined, session);
// Verify that the command isn't executed until after the session is disposed
let isFulfilled = false;
nonSessionCommand = azdataTool.arc.sql.mi.list().then(() => isFulfilled = true);
await sleep(2000);
should(isFulfilled).equal(false, 'The command should not be completed yet');
} finally {
session.dispose();
}
await nonSessionCommand;
verifyExecuteCommandCalledWithArgs(['login'], 0);
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list'], 2);
});
it('multiple commands executed without session context are queued up until session is closed', async function (): Promise<void> {
const session = await azdataTool.acquireSession('', '', '');
let nonSessionCommand1: Promise<any> | undefined = undefined;
let nonSessionCommand2: Promise<any> | undefined = undefined;
try {
// Start one command in the current session
await azdataTool.arc.dc.config.show(undefined, session);
// Verify that neither command is completed until the session is closed
let isFulfilled = false;
nonSessionCommand1 = azdataTool.arc.sql.mi.list().then(() => isFulfilled = true);
nonSessionCommand2 = azdataTool.arc.postgres.server.list().then(() => isFulfilled = true);
await sleep(2000);
should(isFulfilled).equal(false, 'The commands should not be completed yet');
} finally {
session.dispose();
}
await Promise.all([nonSessionCommand1, nonSessionCommand2]);
verifyExecuteCommandCalledWithArgs(['login'], 0);
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list'], 2);
verifyExecuteCommandCalledWithArgs(['arc', 'postgres', 'server', 'list'], 3);
});
it('attempting to acquire a second session while a first is still active queues the second session', async function (): Promise<void> {
const firstSession = await azdataTool.acquireSession('', '', '');
let sessionPromise: Promise<azdataExt.AzdataSession> | undefined = undefined;
let secondSessionCommand: Promise<any> | undefined = undefined;
try {
try {
// Start one command in the current session
await azdataTool.arc.dc.config.show(undefined, firstSession);
// Verify that none of the commands for the second session are completed before the first is disposed
let isFulfilled = false;
sessionPromise = azdataTool.acquireSession('', '', '');
sessionPromise.then(session => {
isFulfilled = true;
secondSessionCommand = azdataTool.arc.sql.mi.list(undefined, session).then(() => isFulfilled = true);
});
await sleep(2000);
should(isFulfilled).equal(false, 'The commands should not be completed yet');
} finally {
firstSession.dispose();
}
} finally {
(await sessionPromise)?.dispose();
}
should(secondSessionCommand).not.equal(undefined, 'The second command should have been queued already');
await secondSessionCommand!;
verifyExecuteCommandCalledWithArgs(['login'], 0);
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
verifyExecuteCommandCalledWithArgs(['login'], 2);
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list'], 3);
});
});
it('version', async function (): Promise<void> {
executeCommandStub.resolves({ stdout: '1.0.0', stderr: '' });
await azdataTool.version();
@@ -473,6 +361,7 @@ describe('azdata', function () {
beforeEach(function (): void {
showInformationMessageStub = sinon.stub(vscode.window, 'showInformationMessage').returns(Promise.resolve(<any>loc.yes));
executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').returns(Promise.resolve({ stdout: '', stderr: '' }));
sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(releaseJson));
});
it('successful update', async function (): Promise<void> {
@@ -577,7 +466,6 @@ describe('azdata', function () {
describe('discoverLatestAvailableAzdataVersion', function (): void {
it('finds latest available version of azdata successfully', async function (): Promise<void> {
sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(releaseJson));
await azdata.discoverLatestAvailableAzdataVersion();
});
});
@@ -706,7 +594,6 @@ async function testWin32UnsuccessfulUpdate() {
}
async function testLinuxSuccessfulUpdate(userRequested = false) {
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').returns(Promise.resolve({ stdout: '0.0.0', stderr: '' }));
executeSudoCommandStub.resolves({ stdout: '0.0.0', stderr: '' });
await azdata.checkAndUpdateAzdata(oldAzdataMock, userRequested);
@@ -740,7 +627,6 @@ async function testDarwinSuccessfulUpdate(userRequested = false) {
async function testWin32SuccessfulUpdate(userRequested = false) {
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
await azdata.checkAndUpdateAzdata(oldAzdataMock, userRequested);
should(executeSudoCommandStub.calledOnce).be.true('executeSudoCommand should have been called once');
@@ -748,7 +634,6 @@ async function testWin32SuccessfulUpdate(userRequested = false) {
}
async function testLinuxSkippedUpdate() {
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
executeSudoCommandStub.resolves({ stdout: '0.0.0', stderr: '' });
await azdata.checkAndUpdateAzdata(currentAzdataMock);
should(executeSudoCommandStub.callCount).be.equal(0, 'executeSudoCommand was not expected to be called');
@@ -778,14 +663,12 @@ async function testDarwinSkippedUpdateDontPrompt() {
}
async function testWin32SkippedUpdateDontPrompt() {
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(executeSudoCommandStub.notCalled).be.true('executeSudoCommand should not have been called');
should(executeSudoCommandStub.notCalled).be.true(`executeSudoCommand should not have been called ${executeSudoCommandStub.getCalls().join(os.EOL)}`);
}
async function testLinuxSkippedUpdateDontPrompt() {
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
sinon.stub(childProcess, 'executeCommand').returns(Promise.resolve({ stdout: '0.0.0', stderr: '' }));
executeSudoCommandStub.resolves({ stdout: '0.0.0', stderr: '' });
await azdata.checkAndUpdateAzdata(oldAzdataMock);
@@ -816,7 +699,6 @@ async function testDarwinSkippedUpdate() {
}
async function testWin32SkippedUpdate() {
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
await azdata.checkAndUpdateAzdata(currentAzdataMock);
should(executeSudoCommandStub.notCalled).be.true('executeSudoCommand should not have been called');
@@ -849,7 +731,6 @@ async function testLinuxSkippedInstall() {
}
async function testWin32SkippedInstall() {
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
sinon.stub(childProcess, 'executeCommand')
.onFirstCall()
@@ -863,7 +744,6 @@ async function testWin32SkippedInstall() {
}
async function testWin32SuccessfulInstall() {
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
.onFirstCall()

View File

@@ -18,7 +18,3 @@ export async function assertRejected(promise: Promise<any>, message: string): Pr
throw new Error(message);
}
export async function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@@ -17,7 +17,7 @@ declare module 'azdata-ext' {
name = 'Microsoft.azdata'
}
export type AdditionalEnvVars = { [key: string]: string};
export type AdditionalEnvVars = { [key: string]: string };
export interface ErrorWithLink extends Error {
messageWithLink: string;
@@ -160,7 +160,7 @@ declare module 'azdata-ext' {
export interface PostgresServerShowResult {
apiVersion: string, // "arcdata.microsoft.com/v1alpha1"
kind: string, // "postgresql-12"
kind: string, // "postgresql"
metadata: {
creationTimestamp: string, // "2020-08-19T20:25:11Z"
generation: number, // 1
@@ -177,7 +177,8 @@ declare module 'azdata-ext' {
}[],
settings: {
default: { [key: string]: string } // { "max_connections": "101", "work_mem": "4MB" }
}
},
version: string // "12"
},
scale: {
shards: number, // 1 (shards was renamed to workers, kept here for backwards compatibility)
@@ -222,6 +223,17 @@ declare module 'azdata-ext' {
state: string, // "Ready"
logSearchDashboard: string, // https://127.0.0.1:30777/kibana/app/kibana#/discover?_a=(query:(language:kuery,query:'custom_resource_name:pg1'))
metricsDashboard: string, // https://127.0.0.1:30777/grafana/d/40q72HnGk/sql-managed-instance-metrics?var-hostname=pg1
podsStatus: {
conditions: {
lastTransitionTime: string, // "2020-08-19T17:05:39Z"
message?: string, // "containers with unready status: [fluentbit postgres telegraf]"
reason?: string, // "ContainersNotReady"
status: string, // "True"
type: string // "Ready"
}[],
name: string, // "pg-instancew-0",
role: string // "worker"
}[]
}
}
@@ -233,25 +245,27 @@ declare module 'azdata-ext' {
code?: number
}
export interface AzdataSession extends vscode.Disposable { }
export interface EndpointOrNamespace {
endpoint?: string,
namespace?: string
}
export interface IAzdataApi {
arc: {
dc: {
create(namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<void>>,
create(namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
endpoint: {
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcEndpointListResult[]>>
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcEndpointListResult[]>>
},
config: {
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcConfigListResult[]>>,
show(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcConfigShowResult>>
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcConfigListResult[]>>,
show(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcConfigShowResult>>
}
},
postgres: {
server: {
delete(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<void>>,
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<PostgresServerListResult[]>>,
show(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<PostgresServerShowResult>>,
delete(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<PostgresServerListResult[]>>,
show(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<PostgresServerShowResult>>,
edit(
name: string,
args: {
@@ -267,17 +281,16 @@ declare module 'azdata-ext' {
replaceEngineSettings?: boolean,
workers?: number
},
engineVersion?: string,
additionalEnvVars?: AdditionalEnvVars,
session?: AzdataSession
azdataContext?: string
): Promise<AzdataOutput<void>>
}
},
sql: {
mi: {
delete(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<void>>,
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<SqlMiListResult[]>>,
show(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<SqlMiShowResult>>,
delete(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<SqlMiListResult[]>>,
show(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<SqlMiShowResult>>,
edit(
name: string,
args: {
@@ -288,22 +301,13 @@ declare module 'azdata-ext' {
noWait?: boolean,
},
additionalEnvVars?: AdditionalEnvVars,
session?: AzdataSession
azdataContext?: string
): Promise<AzdataOutput<void>>
}
}
},
getPath(): Promise<string>,
login(endpoint: string, username: string, password: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzdataOutput<void>>,
/**
* Acquires a session for the specified controller, which will log in to the specified controller and then block all other commands
* that are not part of the original session from executing until the session is released (disposed).
* @param endpoint
* @param username
* @param password
* @param additionalEnvVars
*/
acquireSession(endpoint: string, username: string, password: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzdataSession>,
login(endpointOrNamespace: EndpointOrNamespace, username: string, password: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
/**
* The semVersion corresponding to this installation of azdata. version() method should have been run
* before fetching this value to ensure that correct value is returned. This is almost always correct unless

View File

@@ -317,6 +317,11 @@
}
]
},
"resourceDeploymentValueProviders": [
{
"id": "subscription-id-to-tenant-id"
}
],
"hasAzureResourceProviders": true
},
"dependencies": {

View File

@@ -12,10 +12,10 @@ import {
AzureAccount,
AzureAccountProviderMetadata,
AzureAuthType,
Deferred,
Resource,
Tenant
} from '../interfaces';
} from 'azurecore';
import { Deferred } from '../interfaces';
import * as url from 'url';
import { SimpleTokenCache } from '../simpleTokenCache';
@@ -161,7 +161,7 @@ export abstract class AzureAuth implements vscode.Disposable {
const tenant = account.properties.tenants.find(t => t.id === tenantId);
if (!tenant) {
throw new AzureAuthError(localize('azure.tenantNotFound', "Specifed tenant with ID '{0}' not found.", tenantId), `Tenant ${tenantId} not found.`, undefined);
throw new AzureAuthError(localize('azure.tenantNotFound', "Specified tenant with ID '{0}' not found.", tenantId), `Tenant ${tenantId} not found.`, undefined);
}
const cachedTokens = await this.getSavedToken(tenant, resource, account.key);
@@ -248,7 +248,7 @@ export abstract class AzureAuth implements vscode.Disposable {
if (response.data.error) {
Logger.error('Response error!', response.data);
throw new AzureAuthError(localize('azure.responseError', "Token retrival failed with an error. Open developer tools to view the error"), 'Token retrival failed', undefined);
throw new AzureAuthError(localize('azure.responseError', "Token retrieval failed with an error. Open developer tools to view the error"), 'Token retrieval failed', undefined);
}
const accessTokenString = response.data.access_token;

View File

@@ -4,7 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { AuthorizationCodePostData, AzureAuth, OAuthTokenResponse } from './azureAuth';
import { AzureAccountProviderMetadata, AzureAuthType, Deferred, Resource, Tenant } from '../interfaces';
import { AzureAccountProviderMetadata, AzureAuthType, Resource, Tenant } from 'azurecore';
import { Deferred } from '../interfaces';
import * as vscode from 'vscode';
import * as crypto from 'crypto';
import { SimpleTokenCache } from '../simpleTokenCache';

View File

@@ -18,12 +18,9 @@ import {
AzureAccountProviderMetadata,
AzureAuthType,
Tenant,
Resource,
Deferred,
// Tenant,
// Subscription
} from '../interfaces';
Resource
} from 'azurecore';
import { Deferred } from '../interfaces';
import { SimpleTokenCache } from '../simpleTokenCache';
import { Logger } from '../../utils/Logger';
const localize = nls.loadMessageBundle();

View File

@@ -10,9 +10,9 @@ import * as nls from 'vscode-nls';
import {
AzureAccountProviderMetadata,
AzureAuthType,
Deferred,
AzureAccount
} from './interfaces';
} from 'azurecore';
import { Deferred } from './interfaces';
import { SimpleTokenCache } from './simpleTokenCache';
import { Logger } from '../utils/Logger';
@@ -107,14 +107,14 @@ export class AzureAccountProvider implements azdata.AccountProvider, vscode.Disp
return this._getSecurityToken(account, resource);
}
getAccountSecurityToken(account: azdata.Account, tenant: string, resource: azdata.AzureResource): Thenable<Token | undefined> {
return this._getAccountSecurityToken(account, tenant, resource);
getAccountSecurityToken(account: azdata.Account, tenantId: string, resource: azdata.AzureResource): Thenable<Token | undefined> {
return this._getAccountSecurityToken(account, tenantId, resource);
}
private async _getAccountSecurityToken(account: azdata.Account, tenant: string, resource: azdata.AzureResource): Promise<Token | undefined> {
private async _getAccountSecurityToken(account: azdata.Account, tenantId: string, resource: azdata.AzureResource): Promise<Token | undefined> {
await this.initCompletePromise;
const azureAuth = this.getAuthMethod(undefined);
return azureAuth?.getAccountSecurityToken(account, tenant, resource);
return azureAuth?.getAccountSecurityToken(account, tenantId, resource);
}
private async _getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Promise<MultiTenantTokenResponse | undefined> {

View File

@@ -10,7 +10,8 @@ import * as vscode from 'vscode';
import { SimpleTokenCache } from './simpleTokenCache';
import providerSettings from './providerSettings';
import { AzureAccountProvider as AzureAccountProvider } from './azureAccountProvider';
import { AzureAccountProviderMetadata, ProviderSettings } from './interfaces';
import { AzureAccountProviderMetadata } from 'azurecore';
import { ProviderSettings } from './interfaces';
import * as loc from '../localizedConstants';
let localize = nls.loadMessageBundle();

View File

@@ -3,129 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
/**
* Represents a tenant (an Azure Active Directory instance) to which a user has access
*/
export interface Tenant {
/**
* Globally unique identifier of the tenant
*/
id: string;
/**
* Display name of the tenant
*/
displayName: string;
/**
* Identifier of the user in the tenant
*/
userId?: string;
/**
* The category the user has set their tenant to (e.g. Home Tenant)
*/
tenantCategory?: string;
}
/**
* Represents a resource exposed by an Azure Active Directory
*/
export interface Resource {
/**
* Identifier of the resource
*/
id: string;
/**
* Endpoint url used to access the resource
*/
endpoint: string;
/**
* Resource ID for azdata
*/
azureResourceId?: azdata.AzureResource
}
/**
* Represents settings for an AAD account provider
*/
interface Settings {
/**
* Host of the authority
*/
host?: string;
/**
* Identifier of the client application
*/
clientId?: string;
/**
* Information that describes the Microsoft resource management resource
*/
microsoftResource?: Resource
/**
* Information that describes the AAD graph resource
*/
graphResource?: Resource;
/**
* Information that describes the MS graph resource
*/
msGraphResource?: Resource;
/**
* Information that describes the Azure resource management resource
*/
armResource?: Resource;
/**
* Information that describes the SQL Azure resource
*/
sqlResource?: Resource;
/**
* Information that describes the OSS RDBMS resource
*/
ossRdbmsResource?: Resource;
/**
* Information that describes the Azure Key Vault resource
*/
azureKeyVaultResource?: Resource;
/**
* Information that describes the Azure Dev Ops resource
*/
azureDevOpsResource?: Resource;
/**
* A list of tenant IDs to authenticate against. If defined, then these IDs will be used
* instead of querying the tenants endpoint of the armResource
*/
adTenants?: string[];
// AuthorizationCodeGrantFlowSettings //////////////////////////////////
/**
* An optional site ID that brands the interactive aspect of sign in
*/
siteId?: string;
/**
* Redirect URI that is used to signify the end of the interactive aspect of sign it
*/
redirectUri?: string;
scopes?: string[]
portalEndpoint?: string
}
import * as azurecore from 'azurecore';
/**
* Mapping of configuration key with the metadata to instantiate the account provider
@@ -139,44 +17,7 @@ export interface ProviderSettings {
/**
* Metadata for the provider
*/
metadata: AzureAccountProviderMetadata;
}
/**
* Extension of account provider metadata to override settings type for Azure account providers
*/
export interface AzureAccountProviderMetadata extends azdata.AccountProviderMetadata {
/**
* Azure specific account provider settings.
*/
settings: Settings;
}
export enum AzureAuthType {
AuthCodeGrant = 0,
DeviceCode = 1
}
/**
* Properties specific to an Azure account
*/
interface AzureAccountProperties {
/**
* Auth type of azure used to authenticate this account.
*/
azureAuthType?: AzureAuthType
providerSettings: AzureAccountProviderMetadata;
/**
* Whether or not the account is a Microsoft account
*/
isMsAccount: boolean;
/**
* A list of tenants (aka directories) that the account belongs to
*/
tenants: Tenant[];
metadata: azurecore.AzureAccountProviderMetadata;
}
export interface Subscription {
@@ -185,16 +26,6 @@ export interface Subscription {
displayName: string
}
/**
* Override of the Account type to enforce properties that are AzureAccountProperties
*/
export interface AzureAccount extends azdata.Account {
/**
* AzureAccountProperties specifically used for Azure accounts
*/
properties: AzureAccountProperties;
}
/**
* Token returned from a request for an access token
*/

View File

@@ -76,7 +76,7 @@ export class AzureDataGridProvider implements azdata.DataGridProvider {
{ id: 'icon', type: 'image', field: 'iconPath', name: '', width: 25, sortable: false, filterable: false, resizable: false, tooltip: loc.typeIcon },
{ id: 'name', type: 'text', field: 'name', name: loc.name, width: 150 },
{ id: 'type', type: 'text', field: 'typeDisplayName', name: loc.resourceType, width: 150 },
{ id: 'type', type: 'text', field: 'resourceGroup', name: loc.resourceGroup, width: 150 },
{ id: 'resourceGroup', type: 'text', field: 'resourceGroup', name: loc.resourceGroup, width: 150 },
{ id: 'location', type: 'text', field: 'locationDisplayName', name: loc.location, width: 150 },
{ id: 'subscriptionId', type: 'text', field: 'subscriptionName', name: loc.subscription, width: 150 }
];

View File

@@ -71,6 +71,39 @@ declare module 'azureResource' {
export interface AzureResourceResourceGroup extends AzureResource {
}
export interface AzureLocation {
id: string,
name: string,
displayName: string,
regionalDisplayName: string,
metadata: {
regionType: string,
regionCategory: string,
geographyGroup: string,
longitude: number,
latitude: number,
physicalLocation: string,
pairedRegion: {
name: string,
id: string,
}[],
},
}
export interface AzureSqlManagedInstance extends AzureGraphResource {
}
export interface ManagedDatabase {
id: string,
location: string,
name: string,
properties: {
sourceDatabaseId: string,
status: string
},
type: string
}
export interface AzureResourceDatabase extends AzureSqlResource {
serverName: string;
serverFullName: string;

View File

@@ -17,7 +17,7 @@ import { AzureResourceTreeProvider } from './tree/treeProvider';
import { AzureResourceAccountTreeNode } from './tree/accountTreeNode';
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureTerminalService } from '../azureResource/interfaces';
import { AzureResourceServiceNames } from './constants';
import { AzureAccount, Tenant } from '../account-provider/interfaces';
import { AzureAccount, Tenant } from 'azurecore';
import { FlatAccountTreeNode } from './tree/flatAccountTreeNode';
import { ConnectionDialogTreeProvider } from './tree/connectionDialogTreeProvider';

View File

@@ -8,7 +8,7 @@ import * as msRest from '@azure/ms-rest-js';
import { Account } from 'azdata';
import { azureResource } from 'azureResource';
import { AzureAccount, Tenant } from '../account-provider/interfaces';
import { AzureAccount, Tenant } from 'azurecore';
export interface IAzureResourceSubscriptionService {
getSubscriptions(account: Account, credential: msRest.ServiceClientCredentials, tenantId: string): Promise<azureResource.AzureResourceSubscription[]>;

View File

@@ -9,7 +9,7 @@ import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import * as WS from 'ws';
import { IAzureTerminalService } from '../interfaces';
import { AzureAccount, Tenant } from '../../account-provider/interfaces';
import { AzureAccount, Tenant } from 'azurecore';
const localize = nls.loadMessageBundle();

View File

@@ -21,7 +21,7 @@ import { AzureResourceMessageTreeNode } from '../messageTreeNode';
import { AzureResourceErrorMessageUtil } from '../utils';
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService } from '../../azureResource/interfaces';
import { AzureAccount } from '../../account-provider/interfaces';
import { AzureAccount } from 'azurecore';
export class AzureResourceAccountTreeNode extends AzureResourceContainerTreeNodeBase {
public constructor(

View File

@@ -19,7 +19,7 @@ import { AzureResourceItemType, AzureResourceServiceNames } from '../constants';
import { AzureResourceMessageTreeNode } from '../messageTreeNode';
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService } from '../../azureResource/interfaces';
import { AzureAccount } from '../../account-provider/interfaces';
import { AzureAccount } from 'azurecore';
import { AzureResourceService } from '../resourceService';
import { AzureResourceResourceTreeNode } from '../resourceTreeNode';
import { AzureResourceErrorMessageUtil } from '../utils';

View File

@@ -7,7 +7,7 @@ import { ResourceGraphClient } from '@azure/arm-resourcegraph';
import { TokenCredentials } from '@azure/ms-rest-js';
import axios, { AxiosRequestConfig } from 'axios';
import * as azdata from 'azdata';
import { AzureRestResponse, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult, HttpRequestMethod } from 'azurecore';
import { AzureRestResponse, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult, HttpRequestMethod, GetLocationsResult, GetManagedDatabasesResult } from 'azurecore';
import { azureResource } from 'azureResource';
import { EOL } from 'os';
import * as nls from 'vscode-nls';
@@ -142,6 +142,40 @@ export async function getResourceGroups(appContext: AppContext, account?: azdata
return result;
}
export async function getLocations(appContext: AppContext, account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false): Promise<GetLocationsResult> {
const result: GetLocationsResult = { locations: [], errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants) || !subscription) {
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
return result;
}
await Promise.all(account.properties.tenants.map(async (tenant: { id: string; }) => {
try {
const path = `/subscriptions/${subscription.id}/locations?api-version=2020-01-01`;
const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors);
result.locations.push(...response.response.data.value);
result.errors.push(...response.errors);
} catch (err) {
const error = new Error(localize('azure.accounts.getLocations.queryError', "Error fetching locations for account {0} ({1}) subscription {2} ({3}) tenant {4} : {5}",
account.displayInfo.displayName,
account.displayInfo.userId,
subscription.id,
subscription.name,
tenant.id,
err instanceof Error ? err.message : err));
console.warn(error);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
}));
return result;
}
export async function runResourceQuery<T extends azureResource.AzureGraphResource>(
account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],
@@ -395,6 +429,15 @@ export async function makeHttpRequest(account: azdata.Account, subscription: azu
return result;
}
export async function getManagedDatabases(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, managedInstance: azureResource.AzureSqlManagedInstance, ignoreErrors: boolean): Promise<GetManagedDatabasesResult> {
const path = `/subscriptions/${subscription.id}/resourceGroups/${managedInstance.resourceGroup}/providers/Microsoft.Sql/managedInstances/${managedInstance.name}/databases?api-version=2020-02-02-preview`;
const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors);
return {
databases: response?.response?.data?.value ?? [],
errors: response.errors ? response.errors : []
};
}
export async function getBlobContainers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise<GetBlobContainersResult> {
const path = `/subscriptions/${subscription.id}/resourceGroups/${storageAccount.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}/blobServices/default/containers?api-version=2019-06-01`;
const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors);

View File

@@ -17,6 +17,175 @@ declare module 'azurecore' {
name = 'Microsoft.azurecore'
}
/**
* Override of the Account type to enforce properties that are AzureAccountProperties
*/
export interface AzureAccount extends azdata.Account {
/**
* AzureAccountProperties specifically used for Azure accounts
*/
properties: AzureAccountProperties;
}
/**
* Properties specific to an Azure account
*/
export interface AzureAccountProperties {
/**
* Auth type of azure used to authenticate this account.
*/
azureAuthType?: AzureAuthType
providerSettings: AzureAccountProviderMetadata;
/**
* Whether or not the account is a Microsoft account
*/
isMsAccount: boolean;
/**
* A list of tenants (aka directories) that the account belongs to
*/
tenants: Tenant[];
}
export const enum AzureAuthType {
AuthCodeGrant = 0,
DeviceCode = 1
}
/**
* Extension of account provider metadata to override settings type for Azure account providers
*/
export interface AzureAccountProviderMetadata extends azdata.AccountProviderMetadata {
/**
* Azure specific account provider settings.
*/
settings: Settings;
}
/**
* Represents settings for an AAD account provider
*/
interface Settings {
/**
* Host of the authority
*/
host?: string;
/**
* Identifier of the client application
*/
clientId?: string;
/**
* Information that describes the Microsoft resource management resource
*/
microsoftResource?: Resource
/**
* Information that describes the AAD graph resource
*/
graphResource?: Resource;
/**
* Information that describes the MS graph resource
*/
msGraphResource?: Resource;
/**
* Information that describes the Azure resource management resource
*/
armResource?: Resource;
/**
* Information that describes the SQL Azure resource
*/
sqlResource?: Resource;
/**
* Information that describes the OSS RDBMS resource
*/
ossRdbmsResource?: Resource;
/**
* Information that describes the Azure Key Vault resource
*/
azureKeyVaultResource?: Resource;
/**
* Information that describes the Azure Dev Ops resource
*/
azureDevOpsResource?: Resource;
/**
* A list of tenant IDs to authenticate against. If defined, then these IDs will be used
* instead of querying the tenants endpoint of the armResource
*/
adTenants?: string[];
// AuthorizationCodeGrantFlowSettings //////////////////////////////////
/**
* An optional site ID that brands the interactive aspect of sign in
*/
siteId?: string;
/**
* Redirect URI that is used to signify the end of the interactive aspect of sign it
*/
redirectUri?: string;
scopes?: string[]
portalEndpoint?: string
}
/**
* Represents a resource exposed by an Azure Active Directory
*/
export interface Resource {
/**
* Identifier of the resource
*/
id: string;
/**
* Endpoint url used to access the resource
*/
endpoint: string;
/**
* Resource ID for azdata
*/
azureResourceId?: azdata.AzureResource
}
/**
* Represents a tenant (an Azure Active Directory instance) to which a user has access
*/
export interface Tenant {
/**
* Globally unique identifier of the tenant
*/
id: string;
/**
* Display name of the tenant
*/
displayName: string;
/**
* Identifier of the user in the tenant
*/
userId?: string;
/**
* The category the user has set their tenant to (e.g. Home Tenant)
*/
tenantCategory?: string;
}
/**
* Enumeration of the Azure datacenter regions. See https://docs.microsoft.com/dotnet/api/microsoft.azure.management.resourcemanager.fluent.core.region
*/
@@ -76,7 +245,9 @@ declare module 'azurecore' {
export interface IExtension {
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly?: boolean): Promise<GetSubscriptionsResult>;
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise<GetResourceGroupsResult>;
getLocations(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise<GetLocationsResult>;
getSqlManagedInstances(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlManagedInstancesResult>;
getManagedDatabases(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, managedInstance: azureResource.AzureSqlManagedInstance, ignoreErrors?: boolean): Promise<GetManagedDatabasesResult>;
getSqlServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlServersResult>;
getSqlVMServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlVMServersResult>;
getStorageAccounts(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetStorageAccountResult>;
@@ -106,7 +277,9 @@ declare module 'azurecore' {
export type GetSubscriptionsResult = { subscriptions: azureResource.AzureResourceSubscription[], errors: Error[] };
export type GetResourceGroupsResult = { resourceGroups: azureResource.AzureResourceResourceGroup[], errors: Error[] };
export type GetSqlManagedInstancesResult = { resources: azureResource.AzureGraphResource[], errors: Error[] };
export type GetLocationsResult = { locations: azureResource.AzureLocation[], errors: Error[] };
export type GetSqlManagedInstancesResult = { resources: azureResource.AzureSqlManagedInstance[], errors: Error[] };
export type GetManagedDatabasesResult = { databases: azureResource.ManagedDatabase[], errors: Error[] };
export type GetSqlServersResult = { resources: azureResource.AzureGraphResource[], errors: Error[] };
export type GetSqlVMServersResult = { resources: azureResource.AzureGraphResource[], errors: Error[] };
export type GetStorageAccountResult = { resources: azureResource.AzureGraphResource[], errors: Error[] };

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