Compare commits

...

68 Commits

Author SHA1 Message Date
Karl Burtram
2df67c4f78 Update changelog (#13773) 2020-12-10 20:49:13 -08:00
Benjin Dubishar
81ed7123c6 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>
2020-12-10 23:31:10 -05:00
Alan Ren
4ab0f729e1 fix the recent list (#13770) 2020-12-10 18:35:31 -08:00
Charles Gagnon
95d22a03ae Update required azdata versions (#13762) 2020-12-10 14:04:33 -08:00
Charles Gagnon
94feb1a80d Update STS to revert SqlClient update (#13758) 2020-12-10 13:02:32 -08:00
Sakshi Sharma
254ecc4123 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
2020-12-10 09:50:49 -08:00
Arvind Ranasaria
515b0794b0 Add kube config and kube cluster to arc data controller screens (#13551) 2020-12-10 02:47:39 -08:00
Charles Gagnon
dc8788b77f Add loading text properties for option sources (#13743)
* Add loading text to deployment radio options

* Fix loading race condition

* Update text
2020-12-09 18:32:05 -08:00
Charles Gagnon
147ee53e35 Revert "Added Accounts and Database Backup Page to Migration wizard (#13548)" (#13742)
This reverts commit e169005571.
2020-12-09 16:28:43 -08:00
Charles Gagnon
e7884b8b61 Make loading components not valid and improve RD radio group (#13738) 2020-12-09 13:15:35 -08:00
Aditya Bist
91522caa67 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
2020-12-09 10:54:47 -08:00
Karl Burtram
15b8fa72ec December release readme (#13733) 2020-12-09 10:51:34 -08:00
Kim Santiago
2cf3357301 vbump schema compare and sql database projects (#13730) 2020-12-08 16:01:53 -08:00
Charles Gagnon
aee8bc2759 Fix environment variables for controller create (#13732) 2020-12-08 15:02:56 -08:00
Sai Avishkar Sreerama
adf848c1ef Added developer name to the list of developers. (#13725)
onboarding commit: Added developer name to the list.
2020-12-08 15:56:02 -06:00
Charles Gagnon
7ad328ee95 Lint azdata.d.ts (#13728) 2020-12-08 13:20:17 -08:00
Sakshi Sharma
2f18753b1f Add workspace information in Import UI (#13648)
* Add workspace information in Import UI

* Addressed comments

* Reduced space between Workspace heading and the label
2020-12-08 09:09:33 -08:00
Leila Lali
e182649adc Fixed Schema compare integration tests by adding retry (#13649) 2020-12-08 08:43:58 -08:00
Charles Gagnon
a74119038f Use console.log for retry logging (#13722) 2020-12-08 08:42:45 -08:00
Aasim Khan
e169005571 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
2020-12-07 23:18:07 -08:00
Kim Santiago
b10b52e4fe switch schema compare to use inputbox instead of table headers (#13715) 2020-12-07 17:42:48 -08:00
Charles Gagnon
5f04a4d499 vBump Arc and Azdata (#13717) 2020-12-07 15:43:53 -08:00
Charles Gagnon
d6e1e8eb52 Retry getConfig (#13712)
* Retry getConfig

* Add logging
2020-12-07 15:11:05 -08:00
Leila Lali
9977e83380 Adding unit tests for schema compare service (#13642) 2020-12-07 14:42:38 -08:00
Charles Gagnon
099e94fa2d Rename action config file (#13709)
* Add action for responding to Needs Logs label

* Fix action name

* Rename config file

* remove quotes
2020-12-07 14:41:00 -08:00
Charles Gagnon
7732f5c0d1 Fix action name (#13708)
* Add action for responding to Needs Logs label

* Fix action name
2020-12-07 14:20:27 -08:00
Charles Gagnon
151a18e3ee Add action for responding to Needs Logs label (#13707) 2020-12-07 14:16:04 -08:00
Charles Gagnon
e59de59e61 Add scan suppressions (#13705) 2020-12-07 13:31:59 -08:00
Lucy Zhang
f96fd911c1 Notebooks: Remove result set summary from saved metadata (#13616)
* remove result set summary from metadata

* remove batchId and id from celloutputmetadata

* remove extra line
2020-12-07 12:28:07 -08:00
Charles Gagnon
6c89c61b0d Retry publish and always try adding asset (#13700)
* Retry publish and always try adding asset

* Undo asset upload change

* Add logging
2020-12-07 11:08:04 -08:00
Charles Gagnon
97a4de4111 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
2020-12-07 10:27:37 -08:00
Charles Gagnon
a70dce7855 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
2020-12-04 17:21:30 -08:00
Charles Gagnon
757ac1d4aa Add descriptions and validation to connected mode (#13676) 2020-12-04 16:15:40 -08:00
Monica Gupta
4092b6493b 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>
2020-12-04 15:41:53 -08:00
Chris LaFreniere
6349f1bd49 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
2020-12-04 14:42:24 -08:00
Alan Ren
0c82024cf3 add ability to control the enabled state of checkbox cells (#13644)
* control enabled state of checkbox cells

* add more check
2020-12-04 11:00:09 -08:00
dependabot[bot]
131e0a6b45 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>
2020-12-04 10:59:36 -08:00
Charles Gagnon
aeb22011d2 Remove placeholder on deployment wizards when field is disabled dynamically (#13658) 2020-12-04 09:25:56 -08:00
Charles Gagnon
3d82074656 Remove debug console log (#13669) 2020-12-04 09:25:44 -08:00
Charles Gagnon
6dc07e5804 Add test for dynamic enablement (#13602)
* Add test for dynamic enablement

* update names
2020-12-04 09:15:34 -08:00
Alan Ren
89d515d3ae vbump asde deployment extension (#13662)
do a patch version update, will adjust if the next change is a major one.
2020-12-03 21:03:44 -08:00
Barbara Valdez
9df56c5c0f Normalize path to change (#13660) 2020-12-03 19:17:55 -08:00
Vasu Bhog
048f85d918 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
2020-12-03 19:37:22 -06:00
Sakshi Sharma
4d3443c192 Update Import UI to match other UIs (#13637)
* Update Import UI to match other UIs

* Fixed another bug
2020-12-03 13:30:47 -08:00
Karl Burtram
f69dc6a445 Update package.json (#13626) 2020-12-03 12:51:02 -08:00
Benjin Dubishar
fde031be48 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
2020-12-03 10:33:31 -08:00
Barbara Valdez
08735c9434 add right padding to notebook toolbar action item (#13640)
* add right padding to action item

* remove extra line and add space
2020-12-03 10:23:20 -08:00
Monica Gupta
f748a8c7bb Fix empty column issue (#13641)
Co-authored-by: Monica Gupta <mogupt@microsoft.com>
2020-12-03 09:32:15 -08:00
nasc17
67e3d2ebdb 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
2020-12-03 08:46:48 -08:00
Christopher C
d17ca1561f Delete ConnectionDialogue.ipynb (#13634)
this nb was an attempt at creating a connection dialog. removing
not found in toc
2020-12-03 08:33:51 -08:00
Barbara Valdez
6f731fcd9e add await to thenable method (#13635) 2020-12-02 16:14:26 -08:00
Chris LaFreniere
d86e1eec10 WYSIWYG Improvements to highlight (#13032)
* Improvements to highlight

* wip

* Tests pass

* Leverage escaping mechanism

* Tweak highlight logic

* PR comments
2020-12-02 15:51:40 -08:00
nasc17
cb567989da 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
2020-12-02 15:22:35 -08:00
Charles Gagnon
40675fcadb Revert "Fix windows insiders icons (#13579)" (#13630)
This reverts commit a0ef594792.
2020-12-02 12:58:38 -08:00
Karl Burtram
0299ef1d83 Bump distro to pickup new icons (#13598) 2020-12-02 10:29:34 -08:00
Charles Gagnon
f544ca3be0 vBump azdata and arc extensions (#13620) 2020-12-02 10:19:27 -08:00
Charles Gagnon
273f40e2b3 Re-order summary fields for arc deployment (#13619) 2020-12-02 10:14:44 -08:00
Arvind Ranasaria
8027993ab4 Make 'Script to notebook' consistent with 'Deploy' when user cancels during password re-acquisition for controller (#13557) 2020-12-01 22:57:00 -08:00
Benjin Dubishar
1078d67728 Update tools service to .61 (#13591) 2020-12-01 13:44:15 -08:00
Kim Santiago
593cb45a50 Update open existing dialog icons (#13571)
* update open existing dialog icons

* undo removing folder.svg

* remove max width and max height
2020-12-01 13:01:56 -08:00
Alan Ren
0b1239b755 accessibility support for filtering (#13581) 2020-12-01 10:13:39 -08:00
Charles Gagnon
a0ef594792 Fix windows insiders icons (#13579) 2020-11-30 22:05:34 -08:00
Monica Gupta
e2cf607896 Update kusto release to 0.4.0 for aria fix (#13550)
* aria cluster fix for kusto

* update latest sqltoolsservice version

Co-authored-by: Monica Gupta <mogupt@microsoft.com>
2020-11-30 17:29:37 -08:00
Leila Lali
f0eeb76846 updating to 0.6.0 (#13576) 2020-11-30 16:03:50 -08:00
Charles Gagnon
5da30e6111 Update to CU8 version of BDC book (#13578) 2020-11-30 14:32:23 -08:00
Benjin Dubishar
a9eb6880d4 Adding additional parameter to data workspace provider API (#13570) 2020-11-30 12:52:08 -08:00
Vladimir Chernov
426f1ae99b tableComponent restore focus after grid append command (#13561) 2020-11-30 22:57:28 +03:00
Lucy Zhang
64dd0f0cad dont add column header in continue request (#13568) 2020-11-30 11:29:50 -08:00
159 changed files with 3150 additions and 1119 deletions

View File

@@ -12,6 +12,10 @@
{
"file": "build\\actions\\AutoMerge\\dist\\index.js",
"_justification": "False positive from webpacked code"
},
{
"file": ".devcontainer\\devcontainer.json",
"_justification": "Local development environment - not used in production"
}
]
}

2
.github/label-actions.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
Needs Logs:
comment: "We need more info to debug your particular issue. If you could attach your logs to the issue (ensure no private data is in them), it would help us fix the issue much faster.\n\nTo find your logs:\n\n- Open command palette (Click **View** -> **Command Palette**)\n- Run the command: **`Developer: Open Logs Folder`**\n\nThis will open the log file locally. Please zip up this folder and attach it to the issue."

15
.github/workflows/on-label.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: On Label
on:
issues:
types: [labeled]
jobs:
processLabelAction:
name: Process Label Action
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Process Label Action
uses: hramos/label-actions@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,5 +1,19 @@
# Change Log
## Version 1.25.1
* Release date: December 10, 2020
* Release status: General Availability
* Fixes https://github.com/microsoft/azuredatastudio/issues/13751
## Version 1.25.0
* Release date: December 8, 2020
* Release status: General Availability
* Kusto extension improvements
* SQL Project extension improvements
* Notebook improvements
* Azure Browse Connections Preview performance improvements
* Bug Fixes
## Version 1.24.0
* Release date: November 12, 2020
* 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=2148607
[win-system]: https://go.microsoft.com/fwlink/?linkid=2148907
[win-zip]: https://go.microsoft.com/fwlink/?linkid=2148908
[osx-zip]: https://go.microsoft.com/fwlink/?linkid=2148710
[linux-zip]: https://go.microsoft.com/fwlink/?linkid=2148708
[linux-rpm]: https://go.microsoft.com/fwlink/?linkid=2148709
[linux-deb]: https://go.microsoft.com/fwlink/?linkid=2148806
[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

View File

@@ -43,6 +43,7 @@ function createDefaultConfig(quality: string): Config {
}
function getConfig(quality: string): Promise<Config> {
console.log(`Getting config for quality ${quality}`);
const client = new DocumentClient(process.env['AZURE_DOCUMENTDB_ENDPOINT']!, { masterKey: process.env['AZURE_DOCUMENTDB_MASTERKEY'] });
const collection = 'dbs/builds/colls/config';
const query = {
@@ -52,13 +53,13 @@ function getConfig(quality: string): Promise<Config> {
]
};
return new Promise<Config>((c, e) => {
return retry(() => new Promise<Config>((c, e) => {
client.queryDocuments(collection, query, { enableCrossPartitionQuery: true }).toArray((err, results) => {
if (err && err.code !== 409) { return e(err); }
c(!results || results.length === 0 ? createDefaultConfig(quality) : results[0] as any as Config);
});
});
}));
}
interface Asset {
@@ -86,6 +87,7 @@ function createOrUpdate(commit: string, quality: string, platform: string, type:
updateTries++;
return new Promise<void>((c, e) => {
console.log(`Querying existing documents to update...`);
client.queryDocuments(collection, updateQuery, { enableCrossPartitionQuery: true }).toArray((err, results) => {
if (err) { return e(err); }
if (results.length !== 1) { return e(new Error('No documents')); }
@@ -101,6 +103,7 @@ function createOrUpdate(commit: string, quality: string, platform: string, type:
release.updates[platform] = type;
}
console.log(`Replacing existing document with updated version`);
client.replaceDocument(release._self, release, err => {
if (err && err.code === 409 && updateTries < 5) { return c(update()); }
if (err) { return e(err); }
@@ -112,7 +115,8 @@ function createOrUpdate(commit: string, quality: string, platform: string, type:
});
}
return new Promise<void>((c, e) => {
return retry(() => new Promise<void>((c, e) => {
console.log(`Attempting to create document`);
client.createDocument(collection, release, err => {
if (err && err.code === 409) { return c(update()); }
if (err) { return e(err); }
@@ -120,7 +124,7 @@ function createOrUpdate(commit: string, quality: string, platform: string, type:
console.log('Build successfully published.');
c();
});
});
}));
}
async function assertContainer(blobService: azure.BlobService, quality: string): Promise<void> {
@@ -188,7 +192,6 @@ async function publish(commit: string, quality: string, platform: string, type:
console.log(`Blob ${quality}, ${blobName} already exists, not publishing again.`);
return;
}
console.log('Uploading blobs to Azure storage...');
await uploadBlob(blobService, quality, blobName, file);
@@ -247,6 +250,22 @@ async function publish(commit: string, quality: string, platform: string, type:
await createOrUpdate(commit, quality, platform, type, release, asset, isUpdate);
}
const RETRY_TIMES = 10;
async function retry<T>(fn: () => Promise<T>): Promise<T> {
for (let run = 1; run <= RETRY_TIMES; run++) {
try {
return await fn();
} catch (err) {
if (!/ECONNRESET/.test(err.message)) {
throw err;
}
console.log(`Caught error ${err} - ${run}/${RETRY_TIMES}`);
}
}
throw new Error('Retried too many times');
}
function main(): void {
const commit = process.env['BUILD_SOURCEVERSION'];

View File

@@ -134,11 +134,10 @@
" if arc_admin_password != confirm_password:\n",
" sys.exit(f'Passwords do not match.')\n",
"\n",
"os.environ[\"SPN_CLIENT_ID\"] = spn_client_id\n",
"os.environ[\"SPN_CLIENT_TENANTID\"] = spn_client_tenantid\n",
"if \"AZDATA_NB_VAR_SPN_CLIENT_SECRET\" in os.environ:\n",
" os.environ[\"SPN_CLIENT_SECRET\"] = os.environ[\"AZDATA_NB_VAR_SPN_CLIENT_SECRET\"]\n",
" print(os.environ[\"SPN_CLIENT_TENANTID\"])\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\""
],
"metadata": {

View File

@@ -114,6 +114,8 @@
"# Login to the data controller.\n",
"#\n",
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n",
"os.environ[\"KUBECONFIG\"] = controller_kubeconfig\n",
"os.environ[\"KUBECTL_CONTEXT\"] = controller_kubectl_context\n",
"cmd = f'azdata login -e {controller_endpoint} -u {controller_username}'\n",
"out=run_command()"
],

View File

@@ -114,6 +114,8 @@
"# Login to the data controller.\n",
"#\n",
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n",
"os.environ[\"KUBECONFIG\"] = controller_kubeconfig\n",
"os.environ[\"KUBECTL_CONTEXT\"] = controller_kubectl_context\n",
"cmd = f'azdata login -e {controller_endpoint} -u {controller_username}'\n",
"out=run_command()"
],

View File

@@ -2,14 +2,14 @@
"name": "arc",
"displayName": "%arc.displayName%",
"description": "%arc.description%",
"version": "0.6.3",
"version": "0.7.1",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
"icon": "images/extension.png",
"engines": {
"vscode": "*",
"azdata": ">=1.23.0"
"azdata": ">=1.25.0"
},
"activationEvents": [
"onCommand:arc.connectToController",
@@ -189,7 +189,9 @@
"editable": false,
"options": {
"source": {
"providerId": "arc.controller.config.profiles"
"providerId": "arc.controller.config.profiles",
"loadingText": "%arc.data.controller.cluster.config.profile.loading%",
"loadingCompletedText": "%arc.data.controller.cluster.config.profile.loadingcompleted%"
},
"defaultValue": "azure-arc-aks-default-storage",
"optionsType": "radio"
@@ -200,7 +202,7 @@
]
},
{
"title": "%arc.data.controller.data.controller.create.azureconfig.title%",
"title": "%arc.data.controller.create.azureconfig.title%",
"sections": [
{
"title": "%arc.data.controller.project.details.title%",
@@ -218,7 +220,7 @@
},
{
"type": "azure_locations",
"label": "%arc.data.controller.arc.data.controller.location%",
"label": "%arc.data.controller.location%",
"defaultValue": "eastus",
"required": true,
"locationVariableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_LOCATION",
@@ -244,7 +246,7 @@
"fields": [
{
"type": "readonly_text",
"label": "%arc.data.controller.data.controller.connectivitymode.description%",
"label": "%arc.data.controller.connectivitymode.description%",
"labelWidth": "600px"
},
{
@@ -268,19 +270,38 @@
}
},
{
"label": "%arc.data.controller.spnclient%",
"variableName": "AZDATA_NB_VAR_SPN_CLIENT_ID",
"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.spnclientsecret%",
"variableName": "AZDATA_NB_VAR_SPN_CLIENT_SECRET",
"label": "%arc.data.controller.spclientsecret%",
"description": "%arc.data.controller.spclientsecret.description%",
"variableName": "AZDATA_NB_VAR_SP_CLIENT_SECRET",
"type": "password",
"required": true,
"defaultValue": "",
@@ -290,38 +311,45 @@
}
},
{
"label": "%arc.data.controller.spntenant%",
"variableName": "AZDATA_NB_VAR_SPN_CLIENT_TENANTID",
"label": "%arc.data.controller.sptenantid%",
"description": "%arc.data.controller.sptenantid.description%",
"variableName": "AZDATA_NB_VAR_SP_TENANT_ID",
"type": "text",
"required": true,
"defaultValue": "",
"enabled": {
"target": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE",
"value": "direct"
}
"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%"
}]
}
]
}
]
},
{
"title": "%arc.data.controller.data.controller.create.controllerconfig.title%",
"title": "%arc.data.controller.create.controllerconfig.title%",
"sections": [
{
"title": "%arc.data.controller.data.controller.details.title%",
"title": "%arc.data.controller.details.title%",
"fields": [
{
"type": "readonly_text",
"label": "%arc.data.controller.data.controller.details.description%",
"label": "%arc.data.controller.details.description%",
"labelWidth": "600px"
},
{
"type": "text",
"label": "%arc.data.controller.arc.data.controller.namespace%",
"label": "%arc.data.controller.namespace%",
"validations" : [{
"type": "regex_match",
"regex": "^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$",
"description": "%arc.data.controller.arc.data.controller.namespace.validation.description%"
"description": "%arc.data.controller.namespace.validation.description%"
}],
"defaultValue": "arc",
"required": true,
@@ -329,11 +357,11 @@
},
{
"type": "text",
"label": "%arc.data.controller.arc.data.controller.name%",
"label": "%arc.data.controller.name%",
"validations" : [{
"type": "regex_match",
"regex": "^[a-z0-9]([-.a-z0-9]{0,251}[a-z0-9])?$",
"description": "%arc.data.controller.arc.data.controller.name.validation.description%"
"description": "%arc.data.controller.name.validation.description%"
}],
"defaultValue": "arc-dc",
"required": true,
@@ -374,7 +402,7 @@
]
},
{
"title": "%arc.data.controller.data.controller.create.summary.title%",
"title": "%arc.data.controller.create.summary.title%",
"isSummaryPage": true,
"fieldHeight": "16px",
"sections": [
@@ -528,18 +556,6 @@
{
"title": "%arc.data.controller.summary.azure%",
"fields": [
{
"label": "%arc.data.controller.summary.data.controller.namespace%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE)"
},
{
"label": "%arc.data.controller.summary.data.controller.name%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME)"
},
{
"label": "%arc.data.controller.summary.subscription%",
"type": "readonly_text",
@@ -564,6 +580,18 @@
{
"title": "%arc.data.controller.summary.controller%",
"fields": [
{
"label": "%arc.data.controller.summary.data.controller.namespace%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE)"
},
{
"label": "%arc.data.controller.summary.data.controller.name%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME)"
},
{
"label": "%arc.data.controller.connectivitymode%",
"type": "readonly_text",
@@ -582,7 +610,7 @@
},
{
"name": "azdata",
"version": "20.2.0"
"version": "20.2.5"
}
],
"when": true
@@ -761,7 +789,7 @@
},
{
"name": "azdata",
"version": "20.2.0"
"version": "20.2.5"
}
],
"when": "true"
@@ -1009,7 +1037,7 @@
},
{
"name": "azdata",
"version": "20.2.0"
"version": "20.2.5"
}
],
"when": "true"

View File

@@ -20,18 +20,20 @@
"arc.data.controller.kube.cluster.context": "Cluster context",
"arc.data.controller.cluster.config.profile.title": "Choose the config profile",
"arc.data.controller.cluster.config.profile": "Config profile",
"arc.data.controller.data.controller.create.azureconfig.title": "Azure and Connectivity Configuration",
"arc.data.controller.data.controller.connectivitymode.description": "Select the connectivity mode for the controller.",
"arc.data.controller.data.controller.create.controllerconfig.title": "Controller Configuration",
"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.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.",
"arc.data.controller.data.controller.details.title": "Data controller details",
"arc.data.controller.data.controller.details.description": "Provide a namespace, name and storage class for your Azure Arc data controller. This name will be used to identify your Arc instance for remote management and monitoring.",
"arc.data.controller.arc.data.controller.namespace": "Data controller namespace",
"arc.data.controller.arc.data.controller.namespace.validation.description": "Namespace must consist of lower case alphanumeric characters or '-', start/end with an alphanumeric character, and be 63 characters or fewer in length.",
"arc.data.controller.arc.data.controller.name": "Data controller name",
"arc.data.controller.arc.data.controller.name.validation.description": "Name must consist of lower case alphanumeric characters, '-' or '.', start/end with an alphanumeric character and be 253 characters or less in length.",
"arc.data.controller.arc.data.controller.location": "Location",
"arc.data.controller.details.title": "Data controller details",
"arc.data.controller.details.description": "Provide a namespace, name and storage class for your Azure Arc data controller. This name will be used to identify your Arc instance for remote management and monitoring.",
"arc.data.controller.namespace": "Data controller namespace",
"arc.data.controller.namespace.validation.description": "Namespace must consist of lower case alphanumeric characters or '-', start/end with an alphanumeric character, and be 63 characters or fewer in length.",
"arc.data.controller.name": "Data controller name",
"arc.data.controller.name.validation.description": "Name must consist of lower case alphanumeric characters, '-' or '.', start/end with an alphanumeric character and be 253 characters or less in length.",
"arc.data.controller.location": "Location",
"arc.data.controller.admin.account.title": "Administrator account",
"arc.data.controller.admin.account.name": "Data controller login",
"arc.data.controller.admin.account.password": "Password",
@@ -39,10 +41,16 @@
"arc.data.controller.connectivitymode": "Connectivity Mode",
"arc.data.controller.direct": "Direct",
"arc.data.controller.indirect": "Indirect",
"arc.data.controller.spnclient": "SPN Client ID",
"arc.data.controller.spnclientsecret": "SPN Client Secret",
"arc.data.controller.spntenant": "SPN Tenant ID",
"arc.data.controller.data.controller.create.summary.title": "Review your configuration",
"arc.data.controller.serviceprincipal.description": "When deploying a controller in direct connected mode a Service Principal is required for uploading metrics 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 Application (client) 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 password generated during creation 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",
"arc.data.controller.summary.arc.by.microsoft" : "by Microsoft",
@@ -65,7 +73,8 @@
"arc.data.controller.summary.data.controller.namespace": "Data controller namespace",
"arc.data.controller.summary.controller": "Controller",
"arc.data.controller.summary.location": "Location",
"arc.data.controller.arc.data.controller.agreement": "I accept {0} and {1}.",
"arc.data.controller.agreement": "I accept {0} and {1}.",
"arc.data.controller.readmore": "Read more",
"microsoft.agreement.privacy.statement":"Microsoft Privacy Statement",
"deploy.script.action":"Script to notebook",
"deploy.done.action":"Deploy",

View File

@@ -4,11 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import * as arc from 'arc';
import * as rd from 'resource-deployment';
import * as loc from '../localizedConstants';
import { PasswordToControllerDialog } from '../ui/dialogs/connectControllerDialog';
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
import { ControllerTreeNode } from '../ui/tree/controllerTreeNode';
import { UserCancelledError } from './utils';
export class UserCancelledError extends Error implements rd.ErrorWithType {
public get type(): rd.ErrorType {
return rd.ErrorType.userCancelled;
}
}
export function arcApi(treeDataProvider: AzureArcTreeDataProvider): arc.IExtension {
return {
getRegisteredDataControllers: () => getRegisteredDataControllers(treeDataProvider),
@@ -16,12 +22,13 @@ export function arcApi(treeDataProvider: AzureArcTreeDataProvider): arc.IExtensi
reacquireControllerPassword: (controllerInfo: arc.ControllerInfo) => reacquireControllerPassword(treeDataProvider, controllerInfo)
};
}
export async function reacquireControllerPassword(treeDataProvider: AzureArcTreeDataProvider, controllerInfo: arc.ControllerInfo): Promise<string> {
const dialog = new PasswordToControllerDialog(treeDataProvider);
dialog.showDialog(controllerInfo);
const model = await dialog.waitForClose();
if (!model) {
throw new UserCancelledError();
throw new UserCancelledError(loc.userCancelledError);
}
return model.password;
}

View File

@@ -13,6 +13,11 @@ export interface KubeClusterContext {
isCurrentContext: boolean;
}
/**
* returns the cluster context defined in the {@see configFile}
*
* @param configFile
*/
export function getKubeConfigClusterContexts(configFile: string): Promise<KubeClusterContext[]> {
const config: any = yamljs.load(configFile);
const rawContexts = <any[]>config['contexts'];
@@ -33,6 +38,38 @@ export function getKubeConfigClusterContexts(configFile: string): Promise<KubeCl
return Promise.resolve(contexts);
}
/**
* searches for {@see previousClusterContext} in the array of {@see clusterContexts}.
* if {@see previousClusterContext} was truthy and it was found in {@see clusterContexts}
* then it returns {@see previousClusterContext}
* else it returns the current cluster context from {@see clusterContexts} unless throwIfNotFound was set on input in which case an error is thrown instead.
* else it returns the current cluster context from {@see clusterContexts}
*
*
* @param clusterContexts
* @param previousClusterContext
* @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
return previousClusterContext;
} else {
if (throwIfNotFound) {
throw new Error(loc.clusterContextNotFound(previousClusterContext));
}
}
}
// if not previousClusterContext or throwIfNotFound was false when previousCLusterContext was not found in the clusterContexts
const currentClusterContext = clusterContexts.find(c => c.isCurrentContext)?.name;
throwUnless(currentClusterContext !== undefined, loc.noCurrentClusterContext);
return currentClusterContext;
}
/**
* returns the default kube config file path
*/
export function getDefaultKubeConfigPath(): string {
return path.join(os.homedir(), '.kube', 'config');
}

View File

@@ -9,8 +9,6 @@ import * as vscode from 'vscode';
import { ConnectionMode, IconPath, IconPathHelper } from '../constants';
import * as loc from '../localizedConstants';
export class UserCancelledError extends Error { }
/**
* Converts the resource type name into the localized Display Name for that type.
* @param resourceType The resource type name to convert

View File

@@ -67,7 +67,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<arc.IE
// register option sources
const rdApi = <rd.IExtension>vscode.extensions.getExtension(rd.extension.name)?.exports;
rdApi.registerOptionsSourceProvider(new ArcControllersOptionsSourceProvider(treeDataProvider));
context.subscriptions.push(rdApi.registerOptionsSourceProvider(new ArcControllersOptionsSourceProvider(treeDataProvider)));
return arcApi(treeDataProvider);
}

View File

@@ -61,7 +61,7 @@ export const yes = localize('arc.yes', "Yes");
export const no = localize('arc.no', "No");
export const feedback = localize('arc.feedback', "Feedback");
export const selectConnectionString = localize('arc.selectConnectionString', "Select from available client connection strings below.");
export const addingWokerNodes = localize('arc.addingWokerNodes', "adding worker nodes");
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 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.");
@@ -85,6 +85,8 @@ export const passwordToController = localize('arc.passwordToController', "Provid
export const controllerUrl = localize('arc.controllerUrl', "Controller URL");
export const serverEndpoint = localize('arc.serverEndpoint', "Server Endpoint");
export const controllerName = localize('arc.controllerName', "Name");
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 username = localize('arc.username', "Username");
export const password = localize('arc.password', "Password");
@@ -139,7 +141,6 @@ 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 coresValidationErrorMessage = localize('arc.coresValidationErrorMessage', "Valid CPU resource quantities are strictly positive.");
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");
@@ -172,6 +173,7 @@ 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 connectionRequired = localize('arc.connectionRequired', "A connection is required to show all properties. Click refresh to re-enter connection information");
@@ -202,6 +204,11 @@ export const variableValueFetchForUnsupportedVariable = (variableName: string) =
export const isPasswordFetchForUnsupportedVariable = (variableName: string) => localize('getIsPassword.unknownVariableName', "Attempt to get isPassword for unknown variable:{0}", variableName);
export const noControllerInfoFound = (name: string) => localize('noControllerInfoFound', "Controller Info could not be found with name: {0}", name);
export const noPasswordFound = (controllerName: string) => localize('noPasswordFound', "Password could not be retrieved for controller: {0} and user did not provide a password. Please retry later.", controllerName);
export const clusterContextNotFound = (clusterContext: string) => localize('clusterContextNotFound', "Cluster Context with name: {0} not found in the Kube config file", clusterContext);
export const noCurrentClusterContext = localize('noCurrentClusterContext', "No current cluster context was found in the kube config file");
export const browse = localize('filePicker.browse', "Browse");
export const select = localize('button.label', "Select");
export const noContextFound = (configFile: string) => localize('noContextFound', "No 'contexts' found in the config file: {0}", configFile);
export const noCurrentContextFound = (configFile: string) => localize('noCurrentContextFound', "No context is marked as 'current-context' in the config file: {0}", configFile);
export const noNameInContext = (configFile: string) => localize('noNameInContext', "No name field was found in a cluster context in the config file: {0}", configFile);
export const userCancelledError = localize('userCancelledError', "User cancelled the dialog");

View File

@@ -6,7 +6,7 @@
import { ControllerInfo, ResourceType } from 'arc';
import * as azdataExt from 'azdata-ext';
import * as vscode from 'vscode';
import { UserCancelledError } from '../common/utils';
import { UserCancelledError } from '../common/api';
import * as loc from '../localizedConstants';
import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog';
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
@@ -71,7 +71,7 @@ export class ControllerModel {
await this.treeDataProvider.addOrUpdateController(model.controllerModel, model.password, false);
this._password = model.password;
} else {
throw new UserCancelledError();
throw new UserCancelledError(loc.userCancelledError);
}
}
}

View File

@@ -7,8 +7,9 @@ import { MiaaResourceInfo } from 'arc';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as vscode from 'vscode';
import { UserCancelledError } from '../common/api';
import { Deferred } from '../common/promise';
import { createCredentialId, parseIpAndPort, UserCancelledError } from '../common/utils';
import { createCredentialId, parseIpAndPort } from '../common/utils';
import { credentialNamespace } from '../constants';
import * as loc from '../localizedConstants';
import { ConnectToSqlDialog } from '../ui/dialogs/connectSqlDialog';

View File

@@ -17,7 +17,7 @@ import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
*/
export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourceProvider {
private _cacheManager = new CacheManager<string, string>();
readonly optionsSourceId = 'arc.controllers';
readonly id = 'arc.controllers';
constructor(private _treeProvider: AzureArcTreeDataProvider) { }
async getOptions(): Promise<string[] | azdata.CategoryValue[]> {

View File

@@ -55,7 +55,7 @@ describe('KubeUtils', function (): void {
});
it('throws error when unable to load config file', async () => {
const error = new Error('unknown error accessing file');
sinon.stub(yamljs, 'load').throws(error); //erroring config file load
sinon.stub(yamljs, 'load').throws(error); // simulate an error thrown from config file load
((await tryExecuteAction(() => getKubeConfigClusterContexts(configFile))).error).should.equal(error, `test: getKubeConfigClusterContexts failed`);
});
});

View File

@@ -49,6 +49,7 @@ export class FakeAzdataApi implements azdataExt.IAzdataApi {
replaceEngineSettings?: boolean,
workers?: number
},
_engineVersion?: string,
_additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<void>> { 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: '', name: '', username: '', rememberPassword: false, resources: [] }, info);
const _info: ControllerInfo = Object.assign({ id: uuid(), url: '', kubeConfigFilePath: '', kubeClusterContext: '', name: '', username: '', rememberPassword: false, resources: [] }, info);
super(treeDataProvider!, _info, password);
}

View File

@@ -11,7 +11,8 @@ import * as sinon from 'sinon';
import * as TypeMoq from 'typemoq';
import { v4 as uuid } from 'uuid';
import * as vscode from 'vscode';
import { UserCancelledError } from '../../common/utils';
import * as loc from '../../localizedConstants';
import { UserCancelledError } from '../../common/api';
import { ControllerModel } from '../../models/controllerModel';
import { ConnectToControllerDialog } from '../../ui/dialogs/connectControllerDialog';
import { AzureArcTreeDataProvider } from '../../ui/tree/azureArcTreeDataProvider';
@@ -38,8 +39,8 @@ describe('ControllerModel', function (): void {
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', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
await should(model.azdataLogin()).be.rejectedWith(new UserCancelledError());
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.azdataLogin()).be.rejectedWith(new UserCancelledError(loc.userCancelledError));
});
it('Reads password from cred store', async function (): Promise<void> {
@@ -57,14 +58,14 @@ describe('ControllerModel', function (): void {
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', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
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 model.azdataLogin();
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password), TypeMoq.Times.once());
});
it('Prompt for password when not in cred store', async function (): Promise<void> {
const password = 'password123';
const password = 'password123'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Stub value for testing")]
// Set up cred store to return empty password
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
@@ -80,17 +81,17 @@ describe('ControllerModel', function (): void {
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', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, 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);
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', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
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 model.azdataLogin();
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password), TypeMoq.Times.once());
});
it('Prompt for password when rememberPassword is true but prompt reconnect is true', async function (): Promise<void> {
const password = 'password123';
const password = 'password123'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Stub value for testing")]
// Set up cred store to return a password to start with
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: 'originalPassword' }));
@@ -105,10 +106,10 @@ describe('ControllerModel', function (): void {
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', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, 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 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', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
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 model.azdataLogin(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called');
@@ -116,7 +117,7 @@ describe('ControllerModel', function (): void {
});
it('Prompt for password when we already have a password but prompt reconnect is true', async function (): Promise<void> {
const password = 'password123';
const password = 'password123'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Stub value for testing")]
// Set up cred store to return a password to start with
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: 'originalPassword' }));
@@ -131,11 +132,11 @@ describe('ControllerModel', function (): void {
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', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, 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 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', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, 'originalPassword');
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');
await model.azdataLogin(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called');
@@ -165,6 +166,8 @@ describe('ControllerModel', function (): void {
{
id: uuid(),
url: '127.0.0.1',
kubeConfigFilePath: '/path/to/.kube/config',
kubeClusterContext: 'currentCluster',
username: 'admin',
name: 'arc',
rememberPassword: false,
@@ -177,6 +180,8 @@ describe('ControllerModel', function (): void {
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',
kubeConfigFilePath: '/path/to/.kube/config',
kubeClusterContext: 'currentCluster',
username: 'newUser',
name: 'newName',
rememberPassword: true,

View File

@@ -7,21 +7,44 @@ import * as azdata from 'azdata';
import * as TypeMoq from 'typemoq';
import * as vscode from 'vscode';
export function createModelViewMock() {
interface ModelViewMocks {
mockModelView: TypeMoq.IMock<azdata.ModelView>,
mockModelBuilder: TypeMoq.IMock<azdata.ModelBuilder>,
mockTextBuilder: TypeMoq.IMock<azdata.ComponentBuilder<azdata.TextComponent, azdata.TextComponentProperties>>,
mockInputBoxBuilder: TypeMoq.IMock<azdata.ComponentBuilder<azdata.InputBoxComponent, azdata.InputBoxProperties>>,
mockButtonBuilder: TypeMoq.IMock<azdata.ComponentBuilder<azdata.ButtonComponent, azdata.ButtonProperties>>,
mockRadioButtonBuilder: TypeMoq.IMock<azdata.ComponentBuilder<azdata.RadioButtonComponent, azdata.RadioButtonProperties>>,
mockDivBuilder: TypeMoq.IMock<azdata.DivBuilder>,
mockFlexBuilder: TypeMoq.IMock<azdata.FlexBuilder>,
mockLoadingBuilder: TypeMoq.IMock<azdata.LoadingComponentBuilder>
}
export function createModelViewMock(buttonClickEmitter?: vscode.EventEmitter<any>): ModelViewMocks {
const mockModelBuilder = TypeMoq.Mock.ofType<azdata.ModelBuilder>();
const mockTextBuilder = setupMockComponentBuilder<azdata.TextComponent, azdata.TextComponentProperties>();
const mockInputBoxBuilder = setupMockComponentBuilder<azdata.InputBoxComponent, azdata.InputBoxProperties>();
buttonClickEmitter = buttonClickEmitter ?? new vscode.EventEmitter<any>();
const mockButtonBuilder = setupMockButtonBuilderWithClickEmitter(buttonClickEmitter);
const mockRadioButtonBuilder = setupMockComponentBuilder<azdata.RadioButtonComponent, azdata.RadioButtonProperties>();
const mockDivBuilder = setupMockContainerBuilder<azdata.DivContainer, azdata.DivContainerProperties, azdata.DivBuilder>();
const mockFlexBuilder = setupMockContainerBuilder<azdata.FlexContainer, azdata.ComponentProperties, azdata.FlexBuilder>();
const mockLoadingBuilder = setupMockLoadingBuilder();
mockModelBuilder.setup(b => b.loadingComponent()).returns(() => mockLoadingBuilder.object);
mockModelBuilder.setup(b => b.text()).returns(() => mockTextBuilder.object);
mockModelBuilder.setup(b => b.inputBox()).returns(() => mockInputBoxBuilder.object);
mockModelBuilder.setup(b => b.button()).returns(() => mockButtonBuilder.object);
mockModelBuilder.setup(b => b.radioButton()).returns(() => mockRadioButtonBuilder.object);
mockModelBuilder.setup(b => b.divContainer()).returns(() => mockDivBuilder.object);
mockModelBuilder.setup(b => b.flexContainer()).returns(() => mockFlexBuilder.object);
const mockModelView = TypeMoq.Mock.ofType<azdata.ModelView>();
mockModelView.setup(mv => mv.modelBuilder).returns(() => mockModelBuilder.object);
return { mockModelView, mockModelBuilder, mockTextBuilder, mockInputBoxBuilder, mockRadioButtonBuilder, mockDivBuilder };
return { mockModelView, mockModelBuilder, mockTextBuilder, mockInputBoxBuilder, mockButtonBuilder, mockRadioButtonBuilder, mockDivBuilder, mockFlexBuilder, mockLoadingBuilder };
}
function setupMockButtonBuilderWithClickEmitter(buttonClickEmitter: vscode.EventEmitter<any>): TypeMoq.IMock<azdata.ComponentBuilder<azdata.ButtonComponent, azdata.ButtonProperties>> {
const { mockComponentBuilder: mockButtonBuilder, mockComponent: mockButtonComponent } = setupMockComponentBuilderAndComponent<azdata.ButtonComponent, azdata.ButtonProperties>();
mockButtonComponent.setup(b => b.onDidClick(TypeMoq.It.isAny())).returns(buttonClickEmitter.event);
return mockButtonBuilder;
}
function setupMockLoadingBuilder(
@@ -39,26 +62,44 @@ export function setupMockComponentBuilder<T extends azdata.Component, P extends
mockComponentBuilder?: TypeMoq.IMock<B>,
): TypeMoq.IMock<B> {
mockComponentBuilder = mockComponentBuilder ?? TypeMoq.Mock.ofType<B>();
const returnComponent = TypeMoq.Mock.ofType<T>();
// Need to setup 'then' for when a mocked object is resolved otherwise the test will hang : https://github.com/florinn/typemoq/issues/66
returnComponent.setup((x: any) => x.then).returns(() => { });
setupMockComponentBuilderAndComponent<T, P, B>(mockComponentBuilder, componentGetter);
return mockComponentBuilder;
}
function setupMockComponentBuilderAndComponent<T extends azdata.Component, P extends azdata.ComponentProperties, B extends azdata.ComponentBuilder<T, P> = azdata.ComponentBuilder<T, P>>(
mockComponentBuilder?: TypeMoq.IMock<B>,
componentGetter?: ((props: P) => T)
): { mockComponentBuilder: TypeMoq.IMock<B>, mockComponent: TypeMoq.IMock<T> } {
mockComponentBuilder = mockComponentBuilder ?? TypeMoq.Mock.ofType<B>();
const mockComponent = createComponentMock<T>();
let compProps: P;
mockComponentBuilder.setup(b => b.withProperties(TypeMoq.It.isAny())).callback((props: P) => compProps = props).returns(() => mockComponentBuilder!.object);
mockComponentBuilder.setup(b => b.component()).returns(() => {
return componentGetter ? componentGetter(compProps) : Object.assign<T, P>(Object.assign({}, returnComponent.object), compProps);
return componentGetter ? componentGetter(compProps) : Object.assign<T, P>(Object.assign({}, mockComponent.object), compProps);
});
// For now just have these be passthrough - can hook up additional functionality later if needed
mockComponentBuilder.setup(b => b.withValidation(TypeMoq.It.isAny())).returns(() => mockComponentBuilder!.object);
return mockComponentBuilder;
return { mockComponentBuilder, mockComponent };
}
function createComponentMock<T extends azdata.Component>(): TypeMoq.IMock<T> {
const mockComponent = TypeMoq.Mock.ofType<T>();
// Need to setup 'then' for when a mocked object is resolved otherwise the test will hang : https://github.com/florinn/typemoq/issues/66
mockComponent.setup((x: any) => x.then).returns(() => { });
return mockComponent;
}
export function setupMockContainerBuilder<T extends azdata.Container<any, any>, P extends azdata.ComponentProperties, B extends azdata.ContainerBuilder<T, any, any, any> = azdata.ContainerBuilder<T, any, any, any>>(
mockContainerBuilder?: TypeMoq.IMock<B>
): TypeMoq.IMock<B> {
mockContainerBuilder = mockContainerBuilder ?? setupMockComponentBuilder<T, P, B>();
const items: azdata.Component[] = [];
const mockContainer = createComponentMock<T>(); // T is azdata.Container type so this creates a azdata.Container mock
mockContainer.setup(c => c.items).returns(() => items);
mockContainerBuilder = mockContainerBuilder ?? setupMockComponentBuilder<T, P, B>((_props) => mockContainer.object);
mockContainerBuilder.setup(b => b.withItems(TypeMoq.It.isAny(), TypeMoq.It.isAny())).callback((_items, _itemsStyle) => items.push(..._items)).returns(() => mockContainerBuilder!.object);
// For now just have these be passthrough - can hook up additional functionality later if needed
mockContainerBuilder.setup(b => b.withItems(TypeMoq.It.isAny(), undefined)).returns(() => mockContainerBuilder!.object);
mockContainerBuilder.setup(b => b.withLayout(TypeMoq.It.isAny())).returns(() => mockContainerBuilder!.object);
return mockContainerBuilder;
}

View File

@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* 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 should from 'should';
import * as sinon from 'sinon';
import * as vscode from 'vscode';
import { Deferred } from '../../../common/promise';
import { FilePicker } from '../../../ui/components/filePicker';
import { createModelViewMock } from '../../stubs';
let filePicker: FilePicker;
const initialPath = '/path/to/.kube/config';
const newFilePath = '/path/to/new/.kube/config';
let filePathInputBox: azdata.InputBoxComponent;
let browseButton: azdata.ButtonComponent;
let flexContainer: azdata.FlexContainer;
const browseButtonEmitter = new vscode.EventEmitter<undefined>();
describe('filePicker', function (): void {
beforeEach(async () => {
const { mockModelBuilder, mockInputBoxBuilder, mockButtonBuilder, mockFlexBuilder } = createModelViewMock(browseButtonEmitter);
filePicker = new FilePicker(mockModelBuilder.object, initialPath, (_disposable) => { });
filePathInputBox = mockInputBoxBuilder.object.component();
browseButton = mockButtonBuilder.object.component();
flexContainer = mockFlexBuilder.object.component();
});
afterEach(() => {
sinon.restore();
});
it('browse Button chooses new FilePath', async () => {
should(filePathInputBox.value).should.not.be.undefined();
filePicker.value!.should.equal(initialPath);
flexContainer.items.should.deepEqual([filePathInputBox, browseButton]);
const deferred = new Deferred();
sinon.stub(vscode.window, 'showOpenDialog').callsFake(async (_options) => {
deferred.resolve();
return [vscode.Uri.file(newFilePath)];
});
browseButtonEmitter.fire(undefined); //simulate the click of the browseButton
await deferred;
filePicker.value!.should.equal(newFilePath);
});
describe('getters and setters', async () => {
it('component getter', () => {
should(filePicker.component()).equal(flexContainer);
});
[true, false].forEach(testValue => {
it(`Test readOnly with testValue: ${testValue}`, () => {
filePicker.readOnly = testValue;
filePicker.readOnly!.should.equal(testValue);
});
it(`Test enabled with testValue: ${testValue}`, () => {
filePicker.enabled = testValue;
filePicker.enabled!.should.equal(testValue);
});
});
});
});

View File

@@ -21,11 +21,11 @@ const radioOptionsInfo = <RadioOptionsInfo>{
};
const divItems: azdata.Component[] = [];
let radioOptionsGroup: RadioOptionsGroup;
let loadingComponent: azdata.LoadingComponent;
describe('radioOptionsGroup', function (): void {
beforeEach(async () => {
const { mockModelView, mockRadioButtonBuilder, mockDivBuilder } = createModelViewMock();
const { mockModelBuilder, mockRadioButtonBuilder, mockDivBuilder, mockLoadingBuilder } = createModelViewMock();
mockRadioButtonBuilder.reset(); // reset any previous mock so that we can set our own.
setupMockComponentBuilder<azdata.RadioButtonComponent, azdata.RadioButtonProperties>(
(props) => new FakeRadioButton(props),
@@ -41,8 +41,9 @@ describe('radioOptionsGroup', function (): void {
},
mockDivBuilder
);
radioOptionsGroup = new RadioOptionsGroup(mockModelView.object, (_disposable) => { });
radioOptionsGroup = new RadioOptionsGroup(mockModelBuilder.object, (_disposable) => { });
await radioOptionsGroup.load(async () => radioOptionsInfo);
loadingComponent = mockLoadingBuilder.object.component();
});
it('verify construction and load', async () => {
@@ -72,6 +73,23 @@ describe('radioOptionsGroup', function (): void {
should(label.CSSStyles!.color).not.be.undefined();
label.CSSStyles!.color.should.equal('Red');
});
describe('getters and setters', async () => {
it(`component getter`, () => {
radioOptionsGroup.component().should.deepEqual(loadingComponent);
});
[true, false].forEach(testValue => {
it(`Test readOnly with testValue: ${testValue}`, () => {
radioOptionsGroup.readOnly = testValue;
radioOptionsGroup.readOnly!.should.equal(testValue);
});
it(`Test enabled with testValue: ${testValue}`, () => {
radioOptionsGroup.enabled = testValue;
radioOptionsGroup.enabled!.should.equal(testValue);
});
});
});
});
function verifyRadioGroup() {

View File

@@ -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', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] };
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: [] };
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', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', 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', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
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: [] },
'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', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
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: [] },
'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', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', 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', name: name!, username: 'sa', rememberPassword: true, resources: [] },
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: name!, 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', name: '', username: 'sa', rememberPassword: true, resources: [] },
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: '', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30081',
undefined);
});

View File

@@ -53,7 +53,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', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
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: [] });
await treeDataProvider.addOrUpdateController(controllerModel, '');
should(children.length).equal(1, 'Controller node should be added correctly');
await treeDataProvider.addOrUpdateController(controllerModel, '');
@@ -64,12 +64,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', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] };
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 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', name: 'new-name', username: 'admin', rememberPassword: false, resources: [] };
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 controllerModel2 = new ControllerModel(treeDataProvider, newInfo);
await treeDataProvider.addOrUpdateController(controllerModel2, '');
should(children.length).equal(1, 'Shouldn\'t add duplicate controller node');
@@ -102,7 +102,7 @@ describe('AzureArcTreeDataProvider tests', function (): void {
mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi);
sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object);
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, 'mypassword');
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');
await treeDataProvider.addOrUpdateController(controllerModel, '');
const controllerNode = treeDataProvider.getControllerNode(controllerModel);
const children = await treeDataProvider.getChildren(controllerNode);
@@ -115,8 +115,8 @@ 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', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
const controllerModel2 = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.2', name: 'my-arc', username: 'cloudsa', rememberPassword: true, resources: [] });
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: [] });
await treeDataProvider.addOrUpdateController(controllerModel, '');
await treeDataProvider.addOrUpdateController(controllerModel2, '');
const children = <ControllerTreeNode[]>(await treeDataProvider.getChildren());
@@ -133,20 +133,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', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
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 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', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
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: [] });
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', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
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 miaaModel = new MiaaModel(controllerModel, { name: 'miaa-1', resourceType: ResourceType.sqlManagedInstances }, undefined!, treeDataProvider);
await treeDataProvider.addOrUpdateController(controllerModel, '');
const controllerNode = treeDataProvider.getControllerNode(controllerModel)!;

View File

@@ -31,6 +31,8 @@ declare module 'arc' {
export type ControllerInfo = {
id: string,
kubeConfigFilePath: string,
kubeClusterContext: string
url: string,
name: string,
username: string,

View File

@@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* 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 path from 'path';
import * as vscode from 'vscode';
import * as loc from '../../localizedConstants';
import { IReadOnly } from '../dialogs/connectControllerDialog';
export interface RadioOptionsInfo {
values?: string[],
defaultValue: string
}
export class FilePicker implements IReadOnly {
private _flexContainer: azdata.FlexContainer;
private _filePathInputBox: azdata.InputBoxComponent;
private _filePickerButton: azdata.ButtonComponent;
constructor(
modelBuilder: azdata.ModelBuilder,
initialPath: string, onNewDisposableCreated: (disposable: vscode.Disposable) => void
) {
const buttonWidth = 80;
this._filePathInputBox = modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
value: initialPath,
width: 350
}).component();
this._filePickerButton = modelBuilder.button()
.withProperties<azdata.ButtonProperties>({
label: loc.browse,
width: buttonWidth
}).component();
onNewDisposableCreated(this._filePickerButton.onDidClick(async () => {
const fileUris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: this._filePathInputBox.value ? vscode.Uri.file(path.dirname(this._filePathInputBox.value)) : undefined,
openLabel: loc.select,
filters: undefined /* file type filters */
});
if (!fileUris || fileUris.length === 0) {
return; // This can happen when a user cancels out. we don't throw and the user just won't be able to move on until they select something.
}
const fileUri = fileUris[0]; //we allow the user to select only one file in the dialog
this._filePathInputBox.value = fileUri.fsPath;
}));
this._flexContainer = createFlexContainer(modelBuilder, [this._filePathInputBox, this._filePickerButton]);
}
component(): azdata.Component {
return this._flexContainer;
}
get onTextChanged() {
return this._filePathInputBox.onTextChanged;
}
get value(): string | undefined {
return this._filePathInputBox?.value;
}
get readOnly(): boolean {
return this.enabled;
}
set readOnly(value: boolean) {
this.enabled = value;
}
get enabled(): boolean {
return !!this._flexContainer.enabled && this._flexContainer.items.every(r => r.enabled);
}
set enabled(value: boolean) {
this._flexContainer.items.forEach(r => r.enabled = value);
this._flexContainer.enabled = value;
}
}
function createFlexContainer(modelBuilder: azdata.ModelBuilder, items: azdata.Component[], rowLayout: boolean = true, width?: string | number, height?: string | number, alignItems?: azdata.AlignItemsType, cssStyles?: { [key: string]: string }): azdata.FlexContainer {
const flexFlow = rowLayout ? 'row' : 'column';
alignItems = alignItems || (rowLayout ? 'center' : undefined);
const itemsStyle = rowLayout ? { CSSStyles: { 'margin-right': '5px', } } : {};
const flexLayout: azdata.FlexLayout = { flexFlow: flexFlow, height: height, width: width, alignItems: alignItems };
return modelBuilder.flexContainer().withItems(items, itemsStyle).withLayout(flexLayout).withProperties<azdata.ComponentProperties>({ CSSStyles: cssStyles || {} }).component();
}

View File

@@ -5,24 +5,22 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { getErrorMessage } from '../../common/utils';
import { IReadOnly } from '../dialogs/connectControllerDialog';
export interface RadioOptionsInfo {
values?: string[],
defaultValue: string
}
export class RadioOptionsGroup {
export class RadioOptionsGroup implements IReadOnly {
static id: number = 1;
private _divContainer!: azdata.DivContainer;
private _loadingBuilder: azdata.LoadingComponentBuilder;
private _currentRadioOption!: azdata.RadioButtonComponent;
constructor(private _view: azdata.ModelView, private _onNewDisposableCreated: (disposable: vscode.Disposable) => void, private _groupName: string = `RadioOptionsGroup${RadioOptionsGroup.id++}`) {
const divBuilder = this._view.modelBuilder.divContainer();
const divBuilderWithProperties = divBuilder.withProperties<azdata.DivContainerProperties>({ clickable: false });
this._divContainer = divBuilderWithProperties.component();
const loadingComponentBuilder = this._view.modelBuilder.loadingComponent();
this._loadingBuilder = loadingComponentBuilder.withItem(this._divContainer);
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);
}
public component(): azdata.LoadingComponent {
@@ -37,7 +35,7 @@ export class RadioOptionsGroup {
const options = optionsInfo.values!;
let defaultValue: string = optionsInfo.defaultValue!;
options.forEach((option: string) => {
const radioOption = this._view!.modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({
const radioOption = this._modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({
label: option,
checked: option === defaultValue,
name: this._groupName,
@@ -60,7 +58,7 @@ export class RadioOptionsGroup {
});
}
catch (e) {
const errorLabel = this._view!.modelBuilder.text().withProperties({ value: getErrorMessage(e), CSSStyles: { 'color': 'Red' } }).component();
const errorLabel = this._modelBuilder.text().withProperties({ value: getErrorMessage(e), CSSStyles: { 'color': 'Red' } }).component();
this._divContainer.addItem(errorLabel);
}
this.component().loading = false;
@@ -69,4 +67,21 @@ export class RadioOptionsGroup {
get value(): string | undefined {
return this._currentRadioOption?.value;
}
get readOnly(): boolean {
return this.enabled;
}
set readOnly(value: boolean) {
this.enabled = value;
}
get enabled(): boolean {
return !!this._divContainer.enabled && this._divContainer.items.every(r => r.enabled);
}
set enabled(value: boolean) {
this._divContainer.items.forEach(r => r.enabled = value);
this._divContainer.enabled = value;
}
}

View File

@@ -179,7 +179,6 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
this.coresLimitBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 1,
validationErrorMessage: loc.coresValidationErrorMessage,
inputType: 'number',
placeHolder: loc.loading
}).component();
@@ -197,7 +196,6 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
this.coresRequestBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 1,
validationErrorMessage: loc.coresValidationErrorMessage,
inputType: 'number',
placeHolder: loc.loading
}).component();
@@ -318,6 +316,7 @@ 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;
@@ -328,6 +327,7 @@ 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

@@ -76,7 +76,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
}).component();
const workerNodeslink = this.modelView.modelBuilder.hyperlink().withProperties<azdata.HyperlinkComponentProperties>({
label: loc.addingWokerNodes,
label: loc.addingWorkerNodes,
url: 'https://docs.microsoft.com/azure/azure-arc/data/scale-up-down-postgresql-hyperscale-server-group-using-cli',
CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
@@ -157,7 +157,9 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
async (_progress, _token): Promise<void> => {
try {
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name, this.saveArgs);
this._postgresModel.info.name,
this.saveArgs,
this._postgresModel.engineVersion);
} catch (err) {
// If an error occurs while editing the instance then re-enable the save button since
// the edit wasn't successfully applied
@@ -225,7 +227,6 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.coresLimitBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 1,
validationErrorMessage: loc.coresValidationErrorMessage,
inputType: 'number',
placeHolder: loc.loading
}).component();
@@ -243,7 +244,6 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
this.coresRequestBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 1,
validationErrorMessage: loc.coresValidationErrorMessage,
inputType: 'number',
placeHolder: loc.loading
}).component();
@@ -448,6 +448,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
currentCPUSize = '';
}
this.coresRequestBox!.validationErrorMessage = loc.validationMin(this.coresRequestBox!.min!);
this.coresRequestBox!.placeHolder = currentCPUSize;
this.coresRequestBox!.value = '';
this.saveArgs.coresRequest = undefined;
@@ -458,6 +459,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
currentCPUSize = '';
}
this.coresLimitBox!.validationErrorMessage = loc.validationMin(this.coresLimitBox!.min!);
this.coresLimitBox!.placeHolder = currentCPUSize;
this.coresLimitBox!.value = '';
this.saveArgs.coresLimit = undefined;

View File

@@ -157,6 +157,7 @@ export class PostgresOverviewPage extends DashboardPage {
adminPassword: true,
noWait: true
},
this._postgresModel.engineVersion,
{ 'AZDATA_PASSWORD': password });
vscode.window.showInformationMessage(loc.passwordReset);
}

View File

@@ -14,23 +14,45 @@ import { ControllerModel } from '../../models/controllerModel';
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 { FilePicker } from '../components/filePicker';
export type ConnectToControllerDialogModel = { controllerModel: ControllerModel, password: string };
export interface IReadOnly {
readOnly?: boolean
}
abstract class ControllerDialogBase extends InitializingComponent {
protected _toDispose: vscode.Disposable[] = [];
protected modelBuilder!: azdata.ModelBuilder;
protected dialog: azdata.window.Dialog;
protected urlInputBox!: azdata.InputBoxComponent;
protected kubeConfigInputBox!: FilePicker;
protected clusterContextRadioGroup!: RadioOptionsGroup;
protected nameInputBox!: azdata.InputBoxComponent;
protected usernameInputBox!: azdata.InputBoxComponent;
protected passwordInputBox!: azdata.InputBoxComponent;
protected dispose(): void {
this._toDispose.forEach(disposable => disposable.dispose());
this._toDispose.length = 0; // clear the _toDispose array
}
protected getComponents(): (azdata.FormComponent<azdata.Component> & { layout?: azdata.FormItemLayout | undefined; })[] {
return [
{
component: this.urlInputBox,
title: loc.controllerUrl,
required: true
}, {
component: this.kubeConfigInputBox.component(),
title: loc.controllerKubeConfig,
required: true
}, {
component: this.clusterContextRadioGroup.component(),
title: loc.controllerClusterContext,
required: true
}, {
component: this.nameInputBox,
title: loc.controllerName,
@@ -48,7 +70,7 @@ abstract class ControllerDialogBase extends InitializingComponent {
}
protected abstract fieldToFocusOn(): azdata.Component;
protected readonlyFields(): azdata.InputBoxComponent[] { return []; }
protected readonlyFields(): IReadOnly[] { return []; }
protected initializeFields(controllerInfo: ControllerInfo | undefined, password: string | undefined) {
this.urlInputBox = this.modelBuilder.inputBox()
@@ -57,6 +79,18 @@ abstract class ControllerDialogBase extends InitializingComponent {
// If we have a model then we're editing an existing connection so don't let them modify the URL
readOnly: !!controllerInfo
}).component();
this.kubeConfigInputBox = new FilePicker(
this.modelBuilder,
controllerInfo?.kubeConfigFilePath || getDefaultKubeConfigPath(),
(disposable) => this._toDispose.push(disposable)
);
this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
value: controllerInfo?.kubeConfigFilePath || getDefaultKubeConfigPath()
}).component();
this.clusterContextRadioGroup = new RadioOptionsGroup(this.modelBuilder, (disposable) => this._toDispose.push(disposable));
this.loadRadioGroup(controllerInfo?.kubeClusterContext);
this._toDispose.push(this.kubeConfigInputBox.onTextChanged(() => this.loadRadioGroup(controllerInfo?.kubeClusterContext)));
this.nameInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
value: controllerInfo?.name
@@ -81,10 +115,20 @@ abstract class ControllerDialogBase extends InitializingComponent {
this.dialog = azdata.window.createModelViewDialog(title);
}
private loadRadioGroup(previousClusterContext?: string): void {
this.clusterContextRadioGroup.load(async () => {
const clusters = await getKubeConfigClusterContexts(this.kubeConfigInputBox.value!);
return {
values: clusters.map(c => c.name),
defaultValue: getCurrentClusterContext(clusters, previousClusterContext, false),
};
});
}
public showDialog(controllerInfo?: ControllerInfo, password: string | undefined = undefined): azdata.window.Dialog {
this.id = controllerInfo?.id ?? uuid();
this.resources = controllerInfo?.resources ?? [];
this.dialog.cancelButton.onClick(() => this.handleCancel());
this._toDispose.push(this.dialog.cancelButton.onClick(() => this.handleCancel()));
this.dialog.registerContent(async (view) => {
this.modelBuilder = view.modelBuilder;
this.initializeFields(controllerInfo, password);
@@ -100,7 +144,13 @@ abstract class ControllerDialogBase extends InitializingComponent {
this.initialized = true;
});
this.dialog.registerCloseValidator(async () => await this.validate());
this.dialog.registerCloseValidator(async () => {
const isValidated = await this.validate();
if (isValidated) {
this.dispose();
}
return isValidated;
});
this.dialog.okButton.label = loc.connect;
this.dialog.cancelButton.label = loc.cancel;
azdata.window.openDialog(this.dialog);
@@ -116,6 +166,19 @@ abstract class ControllerDialogBase extends InitializingComponent {
public waitForClose(): Promise<ConnectToControllerDialogModel | undefined> {
return this.completionPromise.promise;
}
protected getControllerInfo(url: string, rememberPassword: boolean = false): ControllerInfo {
return {
id: this.id,
url: url,
kubeConfigFilePath: this.kubeConfigInputBox.value!,
kubeClusterContext: this.clusterContextRadioGroup.value!,
name: this.nameInputBox.value ?? '',
username: this.usernameInputBox.value!,
rememberPassword: rememberPassword,
resources: this.resources
};
}
}
export class ConnectToControllerDialog extends ControllerDialogBase {
@@ -164,14 +227,7 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
if (!/.*:\d*$/.test(url)) {
url = `${url}:30080`;
}
const controllerInfo: ControllerInfo = {
id: this.id,
url: url,
name: this.nameInputBox.value ?? '',
username: this.usernameInputBox.value,
rememberPassword: this.rememberPwCheckBox.checked ?? false,
resources: this.resources
};
const controllerInfo: ControllerInfo = this.getControllerInfo(url, !!this.rememberPwCheckBox.checked);
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
try {
// Validate that we can connect to the controller, this also populates the controllerRegistration from the connection response.
@@ -202,6 +258,8 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
protected readonlyFields() {
return [
this.urlInputBox,
this.kubeConfigInputBox,
this.clusterContextRadioGroup,
this.nameInputBox,
this.usernameInputBox
];
@@ -229,14 +287,7 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
return false;
}
}
const controllerInfo: ControllerInfo = {
id: this.id,
url: this.urlInputBox.value!,
name: this.nameInputBox.value!,
username: this.usernameInputBox.value!,
rememberPassword: false,
resources: []
};
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 = 'arcControllers';
const mementoToken = 'arcDataControllers';
/**
* The TreeDataProvider for the Azure Arc view, which displays a list of registered

View File

@@ -5,7 +5,7 @@
import { MiaaResourceInfo, ResourceInfo, ResourceType } from 'arc';
import * as vscode from 'vscode';
import { UserCancelledError } from '../../common/utils';
import { UserCancelledError } from '../../common/api';
import * as loc from '../../localizedConstants';
import { ControllerModel, Registration } from '../../models/controllerModel';
import { MiaaModel } from '../../models/miaaModel';

View File

@@ -2,7 +2,7 @@
"name": "asde-deployment",
"displayName": "%extension-displayName%",
"description": "%extension-description%",
"version": "0.4.0",
"version": "0.4.1",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",

View File

@@ -2,14 +2,14 @@
"name": "azdata",
"displayName": "%azdata.displayName%",
"description": "%azdata.description%",
"version": "0.4.1",
"version": "0.5.0",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
"icon": "images/extension.png",
"engines": {
"vscode": "*",
"azdata": ">=1.23.0"
"azdata": ">=1.25.0"
},
"activationEvents": [
"*"

View File

@@ -102,10 +102,11 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
replaceEngineSettings?: boolean;
workers?: number;
},
engineVersion?: string,
additionalEnvVars?: { [key: string]: string; }) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.edit(name, args, additionalEnvVars);
return azdataToolService.localAzdata.arc.postgres.server.edit(name, args, engineVersion, additionalEnvVars);
}
}
},

View File

@@ -118,6 +118,7 @@ export class AzdataTool implements azdataExt.IAzdataApi {
replaceEngineSettings?: boolean,
workers?: number
},
engineVersion?: string,
additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<void>> => {
const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name];
if (args.adminPassword) { argsArray.push('--admin-password'); }
@@ -131,6 +132,7 @@ 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);
}
}

View File

@@ -65,7 +65,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<azdata
// register option source(s)
const rdApi = <rd.IExtension>vscode.extensions.getExtension(rd.extension.name)?.exports;
rdApi.registerOptionsSourceProvider(new ArcControllerConfigProfilesOptionsSource(azdataApi));
context.subscriptions.push(rdApi.registerOptionsSourceProvider(new ArcControllerConfigProfilesOptionsSource(azdataApi)));
return azdataApi;
}

View File

@@ -10,7 +10,7 @@ import * as azdataExt from 'azdata-ext';
* Class that provides options sources for an Arc Data Controller
*/
export class ArcControllerConfigProfilesOptionsSource implements rd.IOptionsSourceProvider {
readonly optionsSourceId = 'arc.controller.config.profiles';
readonly id = 'arc.controller.config.profiles';
constructor(private _azdataExtApi: azdataExt.IExtension) { }
async getOptions(): Promise<string[]> {
const isEulaAccepted = await this._azdataExtApi.isEulaAccepted();

View File

@@ -262,6 +262,7 @@ declare module 'azdata-ext' {
replaceEngineSettings?: boolean,
workers?: number
},
engineVersion?: string,
additionalEnvVars?: { [key: string]: string }): Promise<AzdataOutput<void>>
}
},

View File

@@ -8,6 +8,7 @@ import * as vscode from 'vscode';
import { promises as fs } from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as resourceDeployment from 'resource-deployment';
import { AppContext } from './appContext';
import { AzureAccountProviderService } from './account-provider/azureAccountProviderService';
@@ -86,8 +87,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
registerAzureServices(appContext);
const azureResourceTree = new AzureResourceTreeProvider(appContext);
const connectionDialogTree = new ConnectionDialogTreeProvider(appContext);
pushDisposable(vscode.window.registerTreeDataProvider('connectionDialog/azureResourceExplorer', connectionDialogTree));
pushDisposable(vscode.window.registerTreeDataProvider('azureResourceExplorer', azureResourceTree));
pushDisposable(vscode.window.registerTreeDataProvider('connectionDialog/azureResourceExplorer', connectionDialogTree));
pushDisposable(vscode.workspace.onDidChangeConfiguration(e => onDidChangeConfiguration(e), this));
registerAzureResourceCommands(appContext, azureResourceTree, connectionDialogTree);
azdata.dataprotocol.registerDataGridProvider(new AzureDataGridProvider(appContext));
@@ -105,6 +106,40 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
}
});
// Don't block on this since there's a bit of a circular dependency here with the extension activation since resource deployment
// depends on this extension too. It's fine to wait a bit for that to finish before registering the provider
vscode.extensions.getExtension(resourceDeployment.extension.name).activate().then((api: resourceDeployment.IExtension) => {
context.subscriptions.push(api.registerValueProvider({
id: 'subscription-id-to-tenant-id',
getValue: async (triggerValue: string) => {
if (triggerValue === '') {
return '';
}
let accounts: azdata.Account[] = [];
try {
accounts = await azdata.accounts.getAllAccounts();
} catch (err) {
console.warn(`Error fetching accounts for subscription-id-to-tenant-id provider : ${err}`);
return '';
}
for (const account of accounts) {
// Ignore any errors - they'll be logged in the called function and we still want to look
// at any subscriptions that are returned - worst case we'll just return an empty string if we didn't
// find the matching subscription
const subs = await azureResourceUtils.getSubscriptions(appContext, account, true);
const sub = subs.subscriptions.find(sub => sub.id === triggerValue);
if (sub) {
return sub.tenant;
}
}
console.error(`Unable to find subscription with ID ${triggerValue} when mapping subscription ID to tenant ID`);
return '';
}
}));
});
return {
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly: boolean = false): Thenable<azurecore.GetSubscriptionsResult> {
return selectedOnly

View File

@@ -7,4 +7,5 @@
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
/// <reference path='../../../../src/sql/azdata.d.ts'/>
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
/// <reference path='../../../resource-deployment/src/typings/resource-deployment.d.ts'/>
/// <reference types='@types/node'/>

View File

@@ -1,78 +0,0 @@
{
"metadata": {
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"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": "code",
"source": [
"import sys,os,getpass,json,html,time\r\n",
"from string import Template"
],
"metadata": {
"azdata_cell_guid": "1887c716-6e0c-41d1-9d67-cfa93884c0d6"
},
"outputs": [],
"execution_count": 1
},
{
"cell_type": "code",
"source": [
"sql_password = \"\"\r\n",
"sql_port = \"\""
],
"metadata": {
"azdata_cell_guid": "f3de6ea8-1ea8-43d6-9277-836b57d85845"
},
"outputs": [],
"execution_count": 2
},
{
"cell_type": "code",
"source": [
"from IPython.display import *\r\n",
"connectionParameter = '{\"serverName\":\"localhost,' + sql_port + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":\"sa\",\"password\":' + json.dumps(sql_password) + '}'\r\n",
"display(HTML('<br/><a href=\"command:azdata.connect?' + html.escape(connectionParameter)+'\"><font size=\"3\">Click here to connect to SQL Server</font></a><br/>'))\r\n",
"display(HTML('<br/><span style=\"color:red\"><font size=\"2\">NOTE: The SQL Server password is included in this link, you may want to clear the results of this code cell before saving the notebook.</font></span>'))"
],
"metadata": {
"azdata_cell_guid": "8e5e0b41-a27d-4a73-9ba6-c0d3bd7a9a2f"
},
"outputs": [
{
"data": {
"text/plain": "<IPython.core.display.HTML object>",
"text/html": "<br/><a href=\"command:azdata.connect?{&quot;serverName&quot;:&quot;localhost,&quot;,&quot;providerName&quot;:&quot;MSSQL&quot;,&quot;authenticationType&quot;:&quot;SqlLogin&quot;,&quot;userName&quot;:&quot;sa&quot;,&quot;password&quot;:&quot;&quot;}\"><font size=\"3\">Click here to connect to SQL Server</font></a><br/>"
},
"metadata": {},
"output_type": "display_data"
}, {
"data": {
"text/plain": "<IPython.core.display.HTML object>",
"text/html": "<br/><span style=\"color:red\"><font size=\"2\">NOTE: The SQL Server password is included in this link, you may want to clear the results of this code cell before saving the notebook.</font></span>"
},
"metadata": {},
"output_type": "display_data"
}
],
"execution_count": 3
}
]
}

View File

@@ -86,6 +86,7 @@
}
},
"dependencies": {
"ads-extension-telemetry": "^1.0.0",
"htmlparser2": "^3.10.1",
"vscode-nls": "^4.0.0"
},

View File

@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import AdsTelemetryReporter from 'ads-extension-telemetry';
import * as Utils from './utils';
const packageJson = require('../package.json');
let packageInfo = Utils.getPackageInfo(packageJson);
export const TelemetryReporter = new AdsTelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
export enum TelemetryViews {
SelectOperationPage = 'SelectOperationPage'
}

View File

@@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface IPackageInfo {
name: string;
version: string;
aiKey: string;
}
export function getPackageInfo(packageJson: any): IPackageInfo | undefined {
if (packageJson) {
return {
name: packageJson.name,
version: packageJson.version,
aiKey: packageJson.aiKey
};
}
return undefined;
}
/**
* Map an error message into a short name for the type of error.
* @param msg The error message to map
*/
export function getTelemetryErrorType(msg: string): string {
if (msg && msg.indexOf('Object reference not set to an instance of an object') !== -1) {
return 'ObjectReferenceNotSet';
}
else {
return 'Other';
}
}

View File

@@ -240,6 +240,13 @@
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e"
integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==
ads-extension-telemetry@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ads-extension-telemetry/-/ads-extension-telemetry-1.0.0.tgz#840b363a6ad958447819b9bc59fdad3e49de31a9"
integrity sha512-ouxZVECe4tsO0ek0dLdnAZEz1Lrytv1uLbbGZhRbZsHITsUYNjnkKnA471uWh0Dj80s+orvv49/j3/tNBDP/SQ==
dependencies:
vscode-extension-telemetry "0.1.2"
ansi-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
@@ -259,6 +266,31 @@ append-transform@^2.0.0:
dependencies:
default-require-extensions "^3.0.0"
applicationinsights@1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.4.0.tgz#e17e436427b6e273291055181e29832cca978644"
integrity sha512-TV8MYb0Kw9uE2cdu4V/UvTKdOABkX2+Fga9iDz0zqV7FLrNXfmAugWZmmdTx4JoynYkln3d5CUHY3oVSUEbfFw==
dependencies:
cls-hooked "^4.2.2"
continuation-local-storage "^3.2.1"
diagnostic-channel "0.2.0"
diagnostic-channel-publishers "^0.3.2"
async-hook-jl@^1.7.6:
version "1.7.6"
resolved "https://registry.yarnpkg.com/async-hook-jl/-/async-hook-jl-1.7.6.tgz#4fd25c2f864dbaf279c610d73bf97b1b28595e68"
integrity sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==
dependencies:
stack-chain "^1.3.7"
async-listener@^0.6.0:
version "0.6.10"
resolved "https://registry.yarnpkg.com/async-listener/-/async-listener-0.6.10.tgz#a7c97abe570ba602d782273c0de60a51e3e17cbc"
integrity sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==
dependencies:
semver "^5.3.0"
shimmer "^1.1.0"
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@@ -301,6 +333,15 @@ circular-json@^0.3.1:
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==
cls-hooked@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/cls-hooked/-/cls-hooked-4.2.2.tgz#ad2e9a4092680cdaffeb2d3551da0e225eae1908"
integrity sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==
dependencies:
async-hook-jl "^1.7.6"
emitter-listener "^1.0.1"
semver "^5.4.1"
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -323,6 +364,14 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
continuation-local-storage@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz#11f613f74e914fe9b34c92ad2d28fe6ae1db7ffb"
integrity sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==
dependencies:
async-listener "^0.6.0"
emitter-listener "^1.1.1"
convert-source-map@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
@@ -377,6 +426,18 @@ default-require-extensions@^3.0.0:
dependencies:
strip-bom "^4.0.0"
diagnostic-channel-publishers@^0.3.2:
version "0.3.5"
resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.3.5.tgz#a84a05fd6cc1d7619fdd17791c17e540119a7536"
integrity sha512-AOIjw4T7Nxl0G2BoBPhkQ6i7T4bUd9+xvdYizwvG7vVAM1dvr+SDrcUudlmzwH0kbEwdR2V1EcnKT0wAeYLQNQ==
diagnostic-channel@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17"
integrity sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc=
dependencies:
semver "^5.3.0"
diff@3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
@@ -415,6 +476,13 @@ domutils@^1.5.1:
dom-serializer "0"
domelementtype "1"
emitter-listener@^1.0.1, emitter-listener@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8"
integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==
dependencies:
shimmer "^1.2.0"
entities@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
@@ -793,7 +861,7 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
semver@^5.4.1, semver@^5.6.0:
semver@^5.3.0, semver@^5.4.1, semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@@ -803,6 +871,11 @@ semver@^6.0.0, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
shimmer@^1.1.0, shimmer@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337"
integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==
should-equal@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3"
@@ -870,6 +943,11 @@ source-map@^0.6.1:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
stack-chain@^1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285"
integrity sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=
string_decoder@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
@@ -934,6 +1012,13 @@ util-deprecate@^1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
vscode-extension-telemetry@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.2.tgz#049207f5453930888ff68ca925b07bab08f2c955"
integrity sha512-FSbaZKlIH3VKvBJsKw7v5bESWHXzltji2rtjaJeJglpQH4tfClzwHMzlMXUZGiblV++djEzb1gW8mb5E+wxFsg==
dependencies:
applicationinsights "1.4.0"
vscode-nls@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c"

View File

@@ -0,0 +1,18 @@
<svg id="f43e4801-4f55-4d5f-909d-6739feccec92" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100" viewBox="0 0 100 100">
<defs>
<linearGradient id="b7240381-76e4-42bd-96fb-e944ca59ba0a" x1="50" y1="87.092" x2="50" y2="31.71" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#0078d4"/>
<stop offset="1" stop-color="#5ea0ef"/>
</linearGradient>
</defs>
<g>
<path d="M22.615,39.642h54.77V70.185a1.836,1.836,0,0,1-1.836,1.836h-51.1a1.836,1.836,0,0,1-1.836-1.836Z" fill="url(#b7240381-76e4-42bd-96fb-e944ca59ba0a)"/>
<path d="M24.451,27.979h51.1a1.836,1.836,0,0,1,1.836,1.836v9.827H22.615V29.783A1.835,1.835,0,0,1,24.451,27.979Z" fill="#8fc9f9"/>
<g>
<circle cx="27.869" cy="34.304" r="1.713" fill="#0078d4"/>
<circle cx="34.364" cy="34.304" r="1.713" fill="#0078d4"/>
<circle cx="40.86" cy="34.304" r="1.713" fill="#0078d4"/>
</g>
<path d="M22.615,39.642h54.77V70.185a1.836,1.836,0,0,1-1.836,1.836h-51.1a1.836,1.836,0,0,1-1.836-1.836Z" fill="#feffff" opacity="0.07"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,16 @@
<svg id="a7873e7b-9149-4583-acd5-327a6cca8a74" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100" viewBox="0 0 100 100">
<defs>
<linearGradient id="fa1a2df5-b243-4ccd-8fb8-af2408d310fc" x1="324" y1="432.822" x2="324" y2="483.565" gradientTransform="translate(-274 -408)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#5ea0ef"/>
<stop offset="1" stop-color="#0078d4"/>
</linearGradient>
</defs>
<g>
<path d="M24.225,30.625H75.807a1.612,1.612,0,0,1,1.644,1.483V67.892a1.579,1.579,0,0,1-1.644,1.483H24.225a1.612,1.612,0,0,1-1.676-1.483V32.172a1.612,1.612,0,0,1,1.674-1.547Z" fill="url(#fa1a2df5-b243-4ccd-8fb8-af2408d310fc)"/>
<path d="M45.5,36.008h9.284a.452.452,0,0,1,.452.452h0v9.671a.419.419,0,0,1-.419.419H45.7a.451.451,0,0,1-.451-.451V36.428a.418.418,0,0,1,.29-.42Z" fill="#fff"/>
<path d="M45.5,51.58h9.284a.487.487,0,0,1,.452.451V61.7a.42.42,0,0,1-.419.42H45.7a.452.452,0,0,1-.451-.452h0V52a.416.416,0,0,1,.29-.419ZM62.589,36.008h9.285a.452.452,0,0,1,.451.452h0v9.671a.419.419,0,0,1-.419.419H62.75A.451.451,0,0,1,62.3,46.1h0V36.428A.418.418,0,0,1,62.589,36.008Z" fill="#83b9f9"/>
<path d="M62.5,51.58h9.284a.487.487,0,0,1,.452.451V61.7a.42.42,0,0,1-.419.42H62.7a.452.452,0,0,1-.451-.452h0V52a.416.416,0,0,1,.29-.419Z" fill="#83b9f9"/>
<path d="M29.255,51.58h9.284a.487.487,0,0,1,.452.451V61.7a.42.42,0,0,1-.419.42H29.448A.452.452,0,0,1,29,61.67h0V52a.414.414,0,0,1,.29-.419Z" fill="#fff"/>
<path d="M29.255,36.008h9.284a.489.489,0,0,1,.452.452v9.671a.419.419,0,0,1-.419.419H29.448A.451.451,0,0,1,29,46.1h0V36.428a.416.416,0,0,1,.29-.42Z" fill="#fff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#231f20;}.cls-2{fill:#212121;}</style></defs><title>file_16x16</title><polygon class="cls-1" points="13.59 2.21 13.58 2.22 13.58 2.2 13.59 2.21"/><path class="cls-2" d="M8.71,0,14,5.29V16H2V0ZM3,15H13V6H8V1H3ZM9,1.71V5h3.29Z"/></svg>

Before

Width:  |  Height:  |  Size: 351 B

View File

@@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>file_inverse_16x16</title><polygon class="cls-1" points="13.59 2.21 13.58 2.22 13.58 2.2 13.59 2.21"/><path class="cls-1" d="M8.71,0,14,5.29V16H2V0ZM3,15H13V6H8V1H3ZM9,1.71V5h3.29Z"/></svg>

Before

Width:  |  Height:  |  Size: 335 B

View File

@@ -138,6 +138,7 @@
]
},
"dependencies": {
"ads-extension-telemetry": "^1.0.0",
"vscode-nls": "^4.0.0"
},
"devDependencies": {
@@ -146,9 +147,9 @@
"mocha": "^5.2.0",
"mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7",
"typemoq": "^2.1.0",
"vscodetestcover": "^1.1.0",
"should": "^13.2.3",
"sinon": "^9.0.2"
"sinon": "^9.0.2",
"typemoq": "^2.1.0",
"vscodetestcover": "^1.1.0"
}
}

View File

@@ -27,4 +27,9 @@ export class DataWorkspaceExtension implements IExtension {
get defaultProjectSaveLocation(): vscode.Uri | undefined {
return defaultProjectSaveLocation();
}
validateWorkspace(): Promise<boolean> {
return this.workspaceService.validateWorkspace();
}
}

View File

@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import AdsTelemetryReporter from 'ads-extension-telemetry';
import * as Utils from './utils';
const packageJson = require('../package.json');
let packageInfo = Utils.getPackageInfo(packageJson)!;
export const TelemetryReporter = new AdsTelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
export enum TelemetryViews {
}

View File

@@ -29,3 +29,21 @@ async function getFileStatus(path: string): Promise<fs.Stats | undefined> {
}
}
}
export interface IPackageInfo {
name: string;
version: string;
aiKey: string;
}
export function getPackageInfo(packageJson: any): IPackageInfo | undefined {
if (packageJson) {
return {
name: packageJson.name,
version: packageJson.version,
aiKey: packageJson.aiKey
};
}
return undefined;
}

View File

@@ -22,7 +22,7 @@ declare module 'dataworkspace' {
* Add projects to the workspace
* @param projectFiles Uris of project files to add
*/
addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise<void>
addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise<void>;
/**
* Change focus to Projects view
@@ -33,6 +33,11 @@ declare module 'dataworkspace' {
* Returns the default location to save projects
*/
defaultProjectSaveLocation: vscode.Uri | undefined;
/**
* Verifies that a workspace is open or if it should be automatically created
*/
validateWorkspace(): Promise<boolean>;
}
/**
@@ -55,8 +60,9 @@ declare module 'dataworkspace' {
*
* @param name Create a project
* @param location the parent directory of the project
* @param projectTypeId the identifier of the selected project type
*/
createProject(name: string, location: vscode.Uri): Promise<vscode.Uri>;
createProject(name: string, location: vscode.Uri, projectTypeId: string): Promise<vscode.Uri>;
/**
* Gets the supported project types

View File

@@ -84,8 +84,8 @@ export class NewProjectDialog extends DialogBase {
]
};
}),
iconHeight: '50px',
iconWidth: '50px',
iconHeight: '75px',
iconWidth: '75px',
cardWidth: '170px',
cardHeight: '170px',
ariaLabel: constants.TypeTitle,

View File

@@ -22,16 +22,10 @@ export class OpenExistingDialog extends DialogBase {
private _targetTypes = [
{
name: constants.Project,
icon: {
dark: this.extensionContext.asAbsolutePath('images/file_inverse.svg'),
light: this.extensionContext.asAbsolutePath('images/file.svg')
}
icon: this.extensionContext.asAbsolutePath('images/Open_existing_Project.svg')
}, {
name: constants.Workspace,
icon: {
dark: this.extensionContext.asAbsolutePath('images/file_inverse.svg'), // temporary - still waiting for real icon from UX
light: this.extensionContext.asAbsolutePath('images/file.svg')
}
icon: this.extensionContext.asAbsolutePath('images/Open_existing_Workspace.svg')
}
];
@@ -97,8 +91,8 @@ export class OpenExistingDialog extends DialogBase {
]
};
}),
iconHeight: '50px',
iconWidth: '50px',
iconHeight: '100px',
iconWidth: '100px',
cardWidth: '170px',
cardHeight: '170px',
ariaLabel: constants.TypeTitle,

View File

@@ -174,7 +174,7 @@ export class WorkspaceService implements IWorkspaceService {
async createProject(name: string, location: vscode.Uri, projectTypeId: string): Promise<vscode.Uri> {
const provider = ProjectProviderRegistry.getProviderByProjectType(projectTypeId);
if (provider) {
const projectFile = await provider.createProject(name, location);
const projectFile = await provider.createProject(name, location, projectTypeId);
this.addProjectsToWorkspace([projectFile]);
this._onDidWorkspaceProjectsChange.fire();
return projectFile;

View File

@@ -29,7 +29,7 @@ export function createProjectProvider(projectTypes: IProjectType[]): IProjectPro
getProjectTreeDataProvider: (projectFile: vscode.Uri): Promise<vscode.TreeDataProvider<any>> => {
return Promise.resolve(treeDataProvider);
},
createProject: (name: string, location: vscode.Uri): Promise<vscode.Uri> => {
createProject: (name: string, location: vscode.Uri, projectTypeId: string): Promise<vscode.Uri> => {
return Promise.resolve(location);
}
};

View File

@@ -235,6 +235,13 @@
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e"
integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==
ads-extension-telemetry@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ads-extension-telemetry/-/ads-extension-telemetry-1.0.0.tgz#840b363a6ad958447819b9bc59fdad3e49de31a9"
integrity sha512-ouxZVECe4tsO0ek0dLdnAZEz1Lrytv1uLbbGZhRbZsHITsUYNjnkKnA471uWh0Dj80s+orvv49/j3/tNBDP/SQ==
dependencies:
vscode-extension-telemetry "0.1.2"
ansi-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
@@ -254,6 +261,31 @@ append-transform@^2.0.0:
dependencies:
default-require-extensions "^3.0.0"
applicationinsights@1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.4.0.tgz#e17e436427b6e273291055181e29832cca978644"
integrity sha512-TV8MYb0Kw9uE2cdu4V/UvTKdOABkX2+Fga9iDz0zqV7FLrNXfmAugWZmmdTx4JoynYkln3d5CUHY3oVSUEbfFw==
dependencies:
cls-hooked "^4.2.2"
continuation-local-storage "^3.2.1"
diagnostic-channel "0.2.0"
diagnostic-channel-publishers "^0.3.2"
async-hook-jl@^1.7.6:
version "1.7.6"
resolved "https://registry.yarnpkg.com/async-hook-jl/-/async-hook-jl-1.7.6.tgz#4fd25c2f864dbaf279c610d73bf97b1b28595e68"
integrity sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==
dependencies:
stack-chain "^1.3.7"
async-listener@^0.6.0:
version "0.6.10"
resolved "https://registry.yarnpkg.com/async-listener/-/async-listener-0.6.10.tgz#a7c97abe570ba602d782273c0de60a51e3e17cbc"
integrity sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==
dependencies:
semver "^5.3.0"
shimmer "^1.1.0"
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@@ -296,6 +328,15 @@ circular-json@^0.3.1:
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==
cls-hooked@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/cls-hooked/-/cls-hooked-4.2.2.tgz#ad2e9a4092680cdaffeb2d3551da0e225eae1908"
integrity sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==
dependencies:
async-hook-jl "^1.7.6"
emitter-listener "^1.0.1"
semver "^5.4.1"
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -318,6 +359,14 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
continuation-local-storage@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz#11f613f74e914fe9b34c92ad2d28fe6ae1db7ffb"
integrity sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==
dependencies:
async-listener "^0.6.0"
emitter-listener "^1.1.1"
convert-source-map@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
@@ -372,6 +421,18 @@ default-require-extensions@^3.0.0:
dependencies:
strip-bom "^4.0.0"
diagnostic-channel-publishers@^0.3.2:
version "0.3.5"
resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.3.5.tgz#a84a05fd6cc1d7619fdd17791c17e540119a7536"
integrity sha512-AOIjw4T7Nxl0G2BoBPhkQ6i7T4bUd9+xvdYizwvG7vVAM1dvr+SDrcUudlmzwH0kbEwdR2V1EcnKT0wAeYLQNQ==
diagnostic-channel@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17"
integrity sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc=
dependencies:
semver "^5.3.0"
diff@3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
@@ -382,6 +443,13 @@ diff@^4.0.2:
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
emitter-listener@^1.0.1, emitter-listener@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8"
integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==
dependencies:
shimmer "^1.2.0"
escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
@@ -734,7 +802,7 @@ safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
semver@^5.4.1, semver@^5.6.0:
semver@^5.3.0, semver@^5.4.1, semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@@ -744,6 +812,11 @@ semver@^6.0.0, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
shimmer@^1.1.0, shimmer@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337"
integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==
should-equal@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3"
@@ -811,6 +884,11 @@ source-map@^0.6.1:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
stack-chain@^1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285"
integrity sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=
strip-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
@@ -863,6 +941,13 @@ typemoq@^2.1.0:
lodash "^4.17.4"
postinstall-build "^5.0.1"
vscode-extension-telemetry@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.2.tgz#049207f5453930888ff68ca925b07bab08f2c955"
integrity sha512-FSbaZKlIH3VKvBJsKw7v5bESWHXzltji2rtjaJeJglpQH4tfClzwHMzlMXUZGiblV++djEzb1gW8mb5E+wxFsg==
dependencies:
applicationinsights "1.4.0"
vscode-nls@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c"

View File

@@ -16,6 +16,7 @@ export const developers: string[] = [
'ktech99',
'kburtram',
'lucyzhang929',
'saiavishkarsreerama',
'smartguest',
'udeeshagautam',
'VasuBhog'

View File

@@ -17,19 +17,21 @@ import { promisify } from 'util';
const retryCount = 24; // 2 minutes
const dacpac1: string = path.join(__dirname, '../../testData/Database1.dacpac');
suite('Dacpac integration test suite', () => {
suite('Dacpac integration test suite @DacFx@', () => {
suiteSetup(async function () {
await utils.sleep(5000); // To ensure the providers are registered.
console.log(`Start dacpac tests`);
});
test('Deploy and extract dacpac @UNSTABLE@', async function () {
this.timeout(5 * 60 * 1000);
const server = await getStandaloneServer();
await utils.connectToServer(server);
const connectionId = await utils.connectToServer(server);
assert(connectionId, `Failed to connect to "${server.serverName}"`);
const nodes = <azdata.objectexplorer.ObjectExplorerNode[]>await azdata.objectexplorer.getActiveConnectionNodes();
const index = nodes.findIndex(node => node.nodePath.includes(server.serverName));
const ownerUri = await azdata.connection.getUriForConnection(nodes[index].connectionId);
const ownerUri = await azdata.connection.getUriForConnection(connectionId);
const now = new Date();
const databaseName = 'ADS_deployDacpac_' + now.getTime().toString();
@@ -70,12 +72,13 @@ suite('Dacpac integration test suite', () => {
const bacpac1: string = path.join(__dirname, '..', '..', 'testData', 'Database1.bacpac');
test('Import and export bacpac @UNSTABLE@', async function () {
this.timeout(5 * 60 * 1000);
const server = await getStandaloneServer();
await utils.connectToServer(server);
const nodes = <azdata.objectexplorer.ObjectExplorerNode[]>await azdata.objectexplorer.getActiveConnectionNodes();
const index = nodes.findIndex(node => node.nodePath.includes(server.serverName));
const ownerUri = await azdata.connection.getUriForConnection(nodes[index].connectionId);
const connectionId = await utils.connectToServer(server);
assert(connectionId, `Failed to connect to "${server.serverName}"`);
const ownerUri = await azdata.connection.getUriForConnection(connectionId);
const now = new Date();
const databaseName = 'ADS_importBacpac_' + now.getTime().toString();

View File

@@ -12,7 +12,7 @@ import * as os from 'os';
import * as fs from 'fs';
import * as path from 'path';
import * as assert from 'assert';
import { getStandaloneServer } from './testConfig';
import { getStandaloneServer, TestServerProfile } from './testConfig';
import { promisify } from 'util';
let schemaCompareService: mssql.ISchemaCompareService;
@@ -25,7 +25,7 @@ const SERVER_CONNECTION_TIMEOUT: number = 3000;
const retryCount = 24; // 2 minutes
const folderPath = path.join(os.tmpdir(), 'SchemaCompareTest');
suite('Schema compare integration test suite', () => {
suite('Schema compare integration test suite @DacFx@', () => {
suiteSetup(async function () {
let attempts: number = 20;
while (attempts > 0) {
@@ -40,6 +40,7 @@ suite('Schema compare integration test suite', () => {
console.log(`Start schema compare tests`);
});
test('Schema compare dacpac to dacpac comparison and scmp @UNSTABLE@', async function () {
this.timeout(5 * 60 * 1000);
assert(schemaCompareService, 'Schema Compare Service Provider is not available');
const now = new Date();
const operationId = 'testOperationId_' + now.getTime().toString();
@@ -82,16 +83,9 @@ suite('Schema compare integration test suite', () => {
assert(openScmpResult.targetEndpointInfo.packageFilePath === target.packageFilePath, `Expected: target packageFilePath to be ${target.packageFilePath}, Actual: ${openScmpResult.targetEndpointInfo.packageFilePath}`);
});
test('Schema compare database to database comparison, script generation, and scmp @UNSTABLE@', async function () {
this.timeout(5 * 60 * 1000);
let server = await getStandaloneServer();
await utils.connectToServer(server, SERVER_CONNECTION_TIMEOUT);
let nodes = <azdata.objectexplorer.ObjectExplorerNode[]>await azdata.objectexplorer.getActiveConnectionNodes();
assert(nodes.length > 0, `Expecting at least one active connection, actual: ${nodes.length}`);
let index = nodes.findIndex(node => node.nodePath.includes(server.serverName));
assert(index !== -1, `Failed to find server: "${server.serverName}" in OE tree`);
const ownerUri = await azdata.connection.getUriForConnection(nodes[index].connectionId);
const ownerUri = await getConnectionUri(server);
const now = new Date();
const operationId = 'testOperationId_' + now.getTime().toString();
@@ -161,16 +155,9 @@ suite('Schema compare integration test suite', () => {
}
});
test('Schema compare dacpac to database comparison, script generation, and scmp @UNSTABLE@', async function () {
this.timeout(5 * 60 * 1000);
let server = await getStandaloneServer();
await utils.connectToServer(server, SERVER_CONNECTION_TIMEOUT);
let nodes = <azdata.objectexplorer.ObjectExplorerNode[]>await azdata.objectexplorer.getActiveConnectionNodes();
assert(nodes.length > 0, `Expecting at least one active connection, actual: ${nodes.length}`);
let index = nodes.findIndex(node => node.nodePath.includes(server.serverName));
assert(index !== -1, `Failed to find server: "${server.serverName}" in OE tree`);
const ownerUri = await azdata.connection.getUriForConnection(nodes[index].connectionId);
const ownerUri = await getConnectionUri(server);
const now = new Date();
const operationId = 'testOperationId_' + now.getTime().toString();
const targetDB: string = 'ads_schemaCompare_targetDB_' + now.getTime().toString();
@@ -228,6 +215,7 @@ suite('Schema compare integration test suite', () => {
}
});
test('Schema compare dacpac to dacpac comparison with include exclude @UNSTABLE@', async function () {
this.timeout(5 * 60 * 1000);
assert(schemaCompareService, 'Schema Compare Service Provider is not available');
const operationId = 'testOperationId_' + new Date().getTime().toString();
@@ -284,6 +272,17 @@ suite('Schema compare integration test suite', () => {
});
});
async function getConnectionUri(server: TestServerProfile): Promise<string> {
// Connext to server
let connectionId = await utils.connectToServer(server, SERVER_CONNECTION_TIMEOUT);
assert(connectionId, `Failed to connect to "${server.serverName}"`);
// Get connection uri
const ownerUri = await azdata.connection.getUriForConnection(connectionId);
return ownerUri;
}
function assertIncludeExcludeResult(result: mssql.SchemaCompareIncludeExcludeResult, expectedSuccess: boolean, expectedBlockingDependenciesLength: number, expectedAffectedDependenciesLength: number): void {
assert(result.success === expectedSuccess, `Operation success should have been ${expectedSuccess}. Actual: ${result.success}`);
if (result.blockingDependencies) {

View File

@@ -35,9 +35,15 @@ export async function connectToServer(connectionInfo: TestConnectionInfo, timeou
options: {}
};
await ensureConnectionViewOpened();
let result = <azdata.ConnectionResult>await azdata.connection.connect(connectionProfile);
assert(result.connected, `Failed to connect to "${connectionProfile.serverName}", error code: ${result.errorCode}, error message: ${result.errorMessage}`);
// Try connecting 3 times
let result = await retryFunction(
async () => {
let connection = <azdata.ConnectionResult>await azdata.connection.connect(connectionProfile);
assert(connection?.connected, `Failed to connect to "${connectionProfile.serverName}", error code: ${connection.errorCode}, error message: ${connection.errorMessage}`);
return connection;
}, 3);
//workaround
//wait for OE to load
await pollTimeout(async () => {
@@ -180,6 +186,26 @@ export async function runQuery(query: string, ownerUri: string): Promise<azdata.
}
export async function retryFunction<T>(fn: () => Promise<T>, retryCount: number): Promise<T> {
let attempts: number = 1;
while (attempts <= retryCount) {
try {
return await fn();
}
catch (e) {
console.error(`utils.retryFunction: Attempt #${attempts} from ${retryCount} failed. Error: ${e}`);
if (attempts === retryCount) {
throw e;
}
}
await sleep(10000);
attempts++;
}
throw new Error(`utils.retryFunction: Failed after ${attempts} attempts`);
}
export async function assertThrowsAsync(fn: () => Promise<any>, msg: string): Promise<void> {
let f = () => {
// Empty

View File

@@ -1,6 +1,6 @@
{
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
"version": "3.0.0-release.52",
"version": "3.0.0-release.60",
"downloadFileNames": {
"Windows_86": "win-x86-netcoreapp3.1.zip",
"Windows_64": "win-x64-netcoreapp3.1.zip",

View File

@@ -1,6 +1,6 @@
{
"name": "kusto",
"version": "0.3.2",
"version": "0.4.0",
"publisher": "Microsoft",
"aiKey": "AIF-444c3af9-8e69-4462-ab49-4191e6ad1916",
"activationEvents": [

View File

@@ -60,7 +60,7 @@ export class GuestSessionManager {
connectionOptions['serverName'] = documentState.serverName;
connectionOptions['databaseName'] = documentState.databaseName;
connectionOptions['userName'] = 'liveshare';
connectionOptions['password'] = 'liveshare';
connectionOptions['password'] = 'liveshare'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Stub value for testing")]
connectionOptions['authenticationType'] = 'liveshare';
connectionOptions['savePassword'] = false;
connectionOptions['saveProfile'] = false;

View File

@@ -40,7 +40,7 @@ export class StatusProvider {
connectionOptions['serverName'] = args.profile.options['server'];
connectionOptions['databaseName'] = args.profile.options['database'];
connectionOptions['userName'] = 'liveshare';
connectionOptions['password'] = 'liveshare';
connectionOptions['password'] = 'liveshare'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Stub value for testing")]
connectionOptions['authenticationType'] = 'liveshare';
connectionOptions['savePassword'] = false;
connectionOptions['saveProfile'] = false;

View File

@@ -2,7 +2,7 @@
"name": "machine-learning",
"displayName": "%displayName%",
"description": "%description%",
"version": "0.5.0",
"version": "0.6.0",
"publisher": "Microsoft",
"preview": true,
"engines": {

View File

@@ -86,7 +86,7 @@ export function createViewContext(): ViewTestContext {
withProps: () => checkBoxBuilder,
withValidation: () => checkBoxBuilder
};
let inputBox: () => azdata.InputBoxComponent = () => Object.assign({}, componentBase, {
let inputBox: () => azdata.InputBoxComponent = () => Object.assign(<azdata.InputBoxComponent>Object.assign({}, componentBase), {
onTextChanged: onClick.event!,
onEnterKeyPressed: undefined!,
value: ''

View File

@@ -325,7 +325,7 @@
"watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose"
},
"dependencies": {
"highlight.js": "9.15.10",
"highlight.js": "10.4.1",
"markdown-it": "^10.0.0",
"markdown-it-front-matter": "^0.2.1",
"vscode-extension-telemetry": "0.1.1",

View File

@@ -2131,10 +2131,10 @@ he@1.1.1:
resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0=
highlight.js@9.15.10:
version "9.15.10"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.10.tgz#7b18ed75c90348c045eef9ed08ca1319a2219ad2"
integrity sha512-RoV7OkQm0T3os3Dd2VHLNMoaoDVx77Wygln3n9l5YV172XonWG6rgQD3XnF/BuFFZw9A0TJgmMSO8FEWQgvcXw==
highlight.js@10.4.1:
version "10.4.1"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0"
integrity sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg==
hmac-drbg@^1.0.0:
version "1.0.1"

View File

@@ -1,6 +1,6 @@
{
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
"version": "3.0.0-release.59",
"version": "3.0.0-release.64",
"downloadFileNames": {
"Windows_86": "win-x86-netcoreapp3.1.zip",
"Windows_64": "win-x64-netcoreapp3.1.zip",

View File

@@ -9,8 +9,8 @@ import { BookTreeItem } from './bookTreeItem';
import { getPinnedNotebooks, setPinnedBookPathsInConfig, IBookNotebook } from '../common/utils';
export interface IBookPinManager {
pinNotebook(notebook: BookTreeItem): boolean;
unpinNotebook(notebook: BookTreeItem): boolean;
pinNotebook(notebook: BookTreeItem): Promise<boolean>;
unpinNotebook(notebook: BookTreeItem): Promise<boolean>;
}
enum PinBookOperation {
@@ -39,20 +39,20 @@ export class BookPinManager implements IBookPinManager {
return false;
}
pinNotebook(notebook: BookTreeItem): boolean {
return this.isNotebookPinned(notebook.book.contentPath) ? false : this.updatePinnedBooks(notebook, PinBookOperation.Pin);
async pinNotebook(notebook: BookTreeItem): Promise<boolean> {
return this.isNotebookPinned(notebook.book.contentPath) ? false : await this.updatePinnedBooks(notebook, PinBookOperation.Pin);
}
unpinNotebook(notebook: BookTreeItem): boolean {
return this.updatePinnedBooks(notebook, PinBookOperation.Unpin);
async unpinNotebook(notebook: BookTreeItem): Promise<boolean> {
return await this.updatePinnedBooks(notebook, PinBookOperation.Unpin);
}
updatePinnedBooks(notebook: BookTreeItem, operation: PinBookOperation) {
async updatePinnedBooks(notebook: BookTreeItem, operation: PinBookOperation): Promise<boolean> {
let modifiedPinnedBooks = false;
let bookPathToChange: string = notebook.book.contentPath;
let pinnedBooks: IBookNotebook[] = getPinnedNotebooks();
let existingBookIndex = pinnedBooks.map(pinnedBookPath => path.normalize(pinnedBookPath?.notebookPath)).indexOf(bookPathToChange);
let existingBookIndex = pinnedBooks.map(pinnedBookPath => path.normalize(pinnedBookPath?.notebookPath)).indexOf(path.normalize(bookPathToChange));
if (existingBookIndex !== -1 && operation === PinBookOperation.Unpin) {
pinnedBooks.splice(existingBookIndex, 1);
@@ -63,9 +63,8 @@ export class BookPinManager implements IBookPinManager {
modifiedPinnedBooks = true;
}
setPinnedBookPathsInConfig(pinnedBooks);
await setPinnedBookPathsInConfig(pinnedBooks);
this.setPinnedSectionContext();
return modifiedPinnedBooks;
}
}

View File

@@ -117,7 +117,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
async pinNotebook(bookTreeItem: BookTreeItem): Promise<void> {
let bookPathToUpdate = bookTreeItem.book?.contentPath;
if (bookPathToUpdate) {
let pinStatusChanged = this.bookPinManager.pinNotebook(bookTreeItem);
let pinStatusChanged = await this.bookPinManager.pinNotebook(bookTreeItem);
if (pinStatusChanged) {
bookTreeItem.contextValue = 'pinnedNotebook';
}
@@ -127,7 +127,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
async unpinNotebook(bookTreeItem: BookTreeItem): Promise<void> {
let bookPathToUpdate = bookTreeItem.book?.contentPath;
if (bookPathToUpdate) {
let pinStatusChanged = this.bookPinManager.unpinNotebook(bookTreeItem);
let pinStatusChanged = await this.bookPinManager.unpinNotebook(bookTreeItem);
if (pinStatusChanged) {
bookTreeItem.contextValue = 'savedNotebook';
}

View File

@@ -378,13 +378,14 @@ function hasWorkspaceFolders(): boolean {
return workspaceFolders && workspaceFolders.length > 0;
}
export function setPinnedBookPathsInConfig(pinnedNotebookPaths: IBookNotebook[]) {
export async function setPinnedBookPathsInConfig(pinnedNotebookPaths: IBookNotebook[]): Promise<void> {
let config: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration(notebookConfigKey);
let storeInWorspace: boolean = hasWorkspaceFolders();
config.update(pinnedBooksConfigKey, pinnedNotebookPaths, storeInWorspace ? false : vscode.ConfigurationTarget.Global);
await config.update(pinnedBooksConfigKey, pinnedNotebookPaths, storeInWorspace ? false : vscode.ConfigurationTarget.Global);
}
export interface IBookNotebook {
bookPath?: string;
notebookPath: string;

View File

@@ -4,11 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import * as rd from 'resource-deployment';
import { valueProviderService } from './services/valueProviderService';
import { optionsSourcesService } from './services/optionSourcesService';
export function getExtensionApi(): rd.IExtension {
return {
registerOptionsSourceProvider: (provider: rd.IOptionsSourceProvider) => optionsSourcesService.registerOptionsSourceProvider(provider)
registerOptionsSourceProvider: (provider: rd.IOptionsSourceProvider) => optionsSourcesService.registerOptionsSourceProvider(provider),
registerValueProvider: (provider: rd.IValueProvider) => valueProviderService.registerValueProvider(provider)
};
}

View File

@@ -244,6 +244,8 @@ export type ComponentCSSStyles = {
export interface IOptionsSource {
provider?: IOptionsSourceProvider
loadingText?: string,
loadingCompletedText?: string,
readonly variableNames?: { [index: string]: string; };
readonly providerId: string;
}
@@ -261,6 +263,11 @@ export interface DynamicEnablementInfo {
value: string
}
export interface ValueProviderInfo {
providerId: string,
triggerField: string
}
export interface FieldInfoBase {
labelWidth?: string;
inputWidth?: string;
@@ -307,9 +314,8 @@ export interface FieldInfo extends SubFieldInfo, FieldInfoBase {
editable?: boolean; // for editable drop-down,
enabled?: boolean | DynamicEnablementInfo;
isEvaluated?: boolean;
valueLookup?: string; // for fetching dropdown options
validationLookup?: string // for fetching text field validations
validations?: ValidationInfo[];
valueProvider?: ValueProviderInfo;
}
export interface KubeClusterContextFieldInfo extends FieldInfo {

View File

@@ -28,7 +28,9 @@ export const NewResourceGroupAriaLabel = localize('azure.resourceGroup.NewResour
export const realm = localize('deployCluster.Realm', "Realm");
export const unknownFieldTypeError = (type: FieldType) => localize('UnknownFieldTypeError', "Unknown field type: \"{0}\"", type);
export const optionsSourceAlreadyDefined = (optionsSourceId: string) => localize('optionsSource.alreadyDefined', "Options Source with id:{0} is already defined", optionsSourceId);
export const valueProviderAlreadyDefined = (providerId: string) => localize('valueProvider.alreadyDefined', "Value Provider with id:{0} is already defined", providerId);
export const noOptionsSourceDefined = (optionsSourceId: string) => localize('optionsSource.notDefined', "No Options Source defined for id: {0}", optionsSourceId);
export const noValueProviderDefined = (providerId: string) => localize('valueProvider.notDefined', "No Value Provider defined for id: {0}", providerId);
export const variableValueFetchForUnsupportedVariable = (variableName: string) => localize('getVariableValue.unknownVariableName', "Attempt to get variable value for unknown variable:{0}", variableName);
export const isPasswordFetchForUnsupportedVariable = (variableName: string) => localize('getIsPassword.unknownVariableName', "Attempt to get isPassword for unknown variable:{0}", variableName);
export const optionsNotDefined = (fieldType: FieldType) => localize('optionsNotDefined', "FieldInfo.options was not defined for field type: {0}", fieldType);

View File

@@ -3,16 +3,24 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as rd from 'resource-deployment';
import * as loc from '../localizedConstants';
class OptionsSourcesService {
private _optionsSourceStore = new Map<string, rd.IOptionsSourceProvider>();
registerOptionsSourceProvider(provider: rd.IOptionsSourceProvider): void {
if (this._optionsSourceStore.has(provider.optionsSourceId)) {
throw new Error(loc.optionsSourceAlreadyDefined(provider.optionsSourceId));
registerOptionsSourceProvider(provider: rd.IOptionsSourceProvider): vscode.Disposable {
if (this._optionsSourceStore.has(provider.id)) {
throw new Error(loc.optionsSourceAlreadyDefined(provider.id));
}
this._optionsSourceStore.set(provider.optionsSourceId, provider);
this._optionsSourceStore.set(provider.id, provider);
return {
dispose: () => this.unregisterOptionsSourceProvider(provider.id)
};
}
private unregisterOptionsSourceProvider(providerId: string): void {
this._optionsSourceStore.delete(providerId);
}
getOptionsSource(optionsSourceProviderId: string): rd.IOptionsSourceProvider {

View File

@@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* 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 rd from 'resource-deployment';
import * as loc from '../localizedConstants';
class ValueProviderService {
private _valueProviderStore = new Map<string, rd.IValueProvider>();
registerValueProvider(provider: rd.IValueProvider): vscode.Disposable {
if (this._valueProviderStore.has(provider.id)) {
throw new Error(loc.valueProviderAlreadyDefined(provider.id));
}
this._valueProviderStore.set(provider.id, provider);
return {
dispose: () => this.unregisterValueProvider(provider.id)
};
}
private unregisterValueProvider(providerId: string): void {
this._valueProviderStore.delete(providerId);
}
getValueProvider(providerId: string): rd.IValueProvider {
const valueProvider = this._valueProviderStore.get(providerId);
if (valueProvider === undefined) {
throw new Error(loc.noValueProviderDefined(providerId));
}
return valueProvider;
}
}
export const valueProviderService = new ValueProviderService();

View File

@@ -3,8 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as events from 'events';
import * as cp from 'promisify-child-process';
import * as TypeMoq from 'typemoq';
import { Readable } from 'stream';
export class TestChildProcessPromise<T> implements cp.ChildProcessPromise {
@@ -103,3 +106,117 @@ export class TestChildProcessPromise<T> implements cp.ChildProcessPromise {
throw new Error('Method not implemented.');
}
}
export type ComponentAndMockComponentBuilder<C, B> = {
component: C,
mockBuilder: TypeMoq.IMock<B>
};
export function createModelViewMock(): {
modelBuilder: TypeMoq.IMock<azdata.ModelBuilder>,
modelView: TypeMoq.IMock<azdata.ModelView>
} {
const mockModelView = TypeMoq.Mock.ofType<azdata.ModelView>();
const mockModelBuilder = TypeMoq.Mock.ofType<azdata.ModelBuilder>();
const mockTextBuilder = createMockComponentBuilder<azdata.TextComponent>();
const mockGroupContainerBuilder = createMockContainerBuilder<azdata.GroupContainer>();
const mockFormContainerBuilder = createMockFormContainerBuilder();
mockModelBuilder.setup(b => b.text()).returns(() => mockTextBuilder.mockBuilder.object);
mockModelBuilder.setup(b => b.groupContainer()).returns(() => mockGroupContainerBuilder.mockBuilder.object);
mockModelBuilder.setup(b => b.formContainer()).returns(() => mockFormContainerBuilder.object);
mockModelView.setup(mv => mv.modelBuilder).returns(() => mockModelBuilder.object);
return {
modelBuilder: mockModelBuilder,
modelView: mockModelView
};
}
export function createMockComponentBuilder<C extends azdata.Component, B extends azdata.ComponentBuilder<C, any> = azdata.ComponentBuilder<C, any>>(component?: C): ComponentAndMockComponentBuilder<C, B> {
const mockComponentBuilder = TypeMoq.Mock.ofType<B>();
// Create a mocked dynamic component if we don't have a stub instance to use.
// Note that we don't use ofInstance here for the component because there's some limitations around properties that I was
// hitting preventing me from easily using TypeMoq. Passing in the stub instance lets users control the object being stubbed - which means
// they can use things like sinon to then override specific functions if desired.
if (!component) {
const mockComponent = TypeMoq.Mock.ofType<C>();
// Need to setup then for when a dynamic mocked object is resolved otherwise the test will hang : https://github.com/florinn/typemoq/issues/66
mockComponent.setup((x: any) => x.then).returns(() => undefined);
component = mockComponent.object;
}
// For now just have these be passthrough - can hook up additional functionality later if needed
mockComponentBuilder.setup(b => b.withProperties(TypeMoq.It.isAny())).returns(() => mockComponentBuilder.object);
mockComponentBuilder.setup(b => b.withValidation(TypeMoq.It.isAny())).returns(() => mockComponentBuilder.object);
mockComponentBuilder.setup(b => b.component()).returns(() => component! /*mockComponent.object*/);
return {
component: component!,
mockBuilder: mockComponentBuilder
};
}
export function createMockContainerBuilder<C extends azdata.Container<any, any>, B extends azdata.ContainerBuilder<C, any, any, any> = azdata.ContainerBuilder<C, any, any, any>>(): ComponentAndMockComponentBuilder<C, B> {
const mockContainerBuilder = createMockComponentBuilder<C, B>();
// For now just have these be passthrough - can hook up additional functionality later if needed
mockContainerBuilder.mockBuilder.setup(b => b.withItems(TypeMoq.It.isAny(), undefined)).returns(() => mockContainerBuilder.mockBuilder.object);
mockContainerBuilder.mockBuilder.setup(b => b.withLayout(TypeMoq.It.isAny())).returns(() => mockContainerBuilder.mockBuilder.object);
return mockContainerBuilder;
}
export function createMockFormContainerBuilder(): TypeMoq.IMock<azdata.FormBuilder> {
const mockContainerBuilder = createMockContainerBuilder<azdata.FormContainer, azdata.FormBuilder>();
mockContainerBuilder.mockBuilder.setup(b => b.withFormItems(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => mockContainerBuilder.mockBuilder.object);
return mockContainerBuilder.mockBuilder;
}
export class StubInputBox implements azdata.InputBoxComponent {
readonly id = 'input-box';
public enabled: boolean = false;
onTextChanged: vscode.Event<any> = undefined!;
onEnterKeyPressed: vscode.Event<string> = undefined!;
updateProperties(properties: { [key: string]: any }): Thenable<void> { throw new Error('Not implemented'); }
updateProperty(key: string, value: any): Thenable<void> { throw new Error('Not implemented'); }
updateCssStyles(cssStyles: { [key: string]: string }): Thenable<void> { throw new Error('Not implemented'); }
readonly onValidityChanged: vscode.Event<boolean> = undefined!;
readonly valid: boolean = true;
validate(): Thenable<boolean> { throw new Error('Not implemented'); }
focus(): Thenable<void> { return Promise.resolve(); }
}
export class StubCheckbox implements azdata.CheckBoxComponent {
private _onChanged = new vscode.EventEmitter<void>();
private _checked = false;
readonly id = 'stub-checkbox';
public enabled: boolean = false;
get checked(): boolean {
return this._checked;
}
set checked(value: boolean) {
this._checked = value;
this._onChanged.fire();
}
onChanged: vscode.Event<any> = this._onChanged.event;
updateProperties(properties: { [key: string]: any }): Thenable<void> { throw new Error('Not implemented'); }
updateProperty(key: string, value: any): Thenable<void> { throw new Error('Not implemented'); }
updateCssStyles(cssStyles: { [key: string]: string }): Thenable<void> { throw new Error('Not implemented'); }
readonly onValidityChanged: vscode.Event<boolean> = undefined!;
readonly valid: boolean = true;
validate(): Thenable<boolean> { throw new Error('Not implemented'); }
focus(): Thenable<void> { return Promise.resolve(); }
}

View File

@@ -0,0 +1,107 @@
/*---------------------------------------------------------------------------------------------
* 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 'mocha';
import * as vscode from 'vscode';
import * as TypeMoq from 'typemoq';
import { initializeWizardPage, InputComponent, InputComponentInfo, Validator, WizardPageContext } from '../../../ui/modelViewUtils';
import { FieldType } from '../../../interfaces';
import { IToolsService } from '../../../services/toolsService';
import { Deferred } from '../../utils';
import { createMockComponentBuilder, createModelViewMock as createMockModelView, StubCheckbox, StubInputBox } from '../../stubs';
import * as should from 'should';
import * as sinon from 'sinon';
describe('WizardPage', () => {
let mockModelBuilder: TypeMoq.IMock<azdata.ModelBuilder>;
let testWizardPage: WizardPageContext;
let contentRegistered: Deferred<void>;
before(function () {
contentRegistered = new Deferred<void>();
const mockWizardPage = TypeMoq.Mock.ofType<azdata.window.WizardPage>();
const mockModelView = createMockModelView();
mockModelBuilder = mockModelView.modelBuilder;
mockWizardPage.setup(p => p.registerContent(TypeMoq.It.isAny())).callback(async (handler: (view: azdata.ModelView) => Thenable<void>) => {
await handler(mockModelView.modelView.object);
contentRegistered.resolve();
});
const mockWizard = TypeMoq.Mock.ofType<azdata.window.Wizard>();
const mockToolsService = TypeMoq.Mock.ofType<IToolsService>();
testWizardPage = {
page: mockWizardPage.object,
container: mockWizard.object,
wizardInfo: {
title: 'TestWizard',
pages: [],
doneAction: {}
},
pageInfo: {
title: 'TestWizardPage',
sections: [
{
fields: [
{
label: 'Field1',
type: FieldType.Checkbox
},
{
label: 'Field2',
type: FieldType.Text,
enabled: {
target: 'Field1',
value: 'true'
}
}
]
}
]
},
inputComponents: {},
onNewDisposableCreated: (_disposable: vscode.Disposable): void => { },
onNewInputComponentCreated: (
name: string,
inputComponentInfo: InputComponentInfo<InputComponent>
): void => {
testWizardPage.inputComponents[name] = inputComponentInfo;
},
onNewValidatorCreated: (_validator: Validator): void => { },
toolsService: mockToolsService.object
};
});
it('dynamic enablement', async function (): Promise<void> {
const stubCheckbox = new StubCheckbox();
const mockCheckboxBuilder = createMockComponentBuilder<azdata.CheckBoxComponent>(stubCheckbox);
const stubInputBox = new StubInputBox();
// Stub out the enabled property so we can hook into when that's set to ensure we wait for the state to be updated
// before continuing the test
let enabled = false;
sinon.stub(stubInputBox, 'enabled').set(v => {
enabled = v;
enabledDeferred.resolve();
});
sinon.stub(stubInputBox, 'enabled').get(() => {
return enabled;
});
const mockInputBoxBuilder = createMockComponentBuilder<azdata.InputBoxComponent>(stubInputBox);
// Used to ensure that we wait until the enabled state is updated for our mocked components before continuing
let enabledDeferred = new Deferred();
mockModelBuilder.setup(b => b.checkBox()).returns(() => mockCheckboxBuilder.mockBuilder.object);
mockModelBuilder.setup(b => b.inputBox()).returns(() => mockInputBoxBuilder.mockBuilder.object);
initializeWizardPage(testWizardPage);
await contentRegistered.promise;
await enabledDeferred.promise;
should(stubInputBox.enabled).be.false('Input box should be disabled by default');
enabledDeferred = new Deferred();
stubCheckbox.checked = true;
// Now wait for the enabled state to be updated again
await enabledDeferred.promise;
should(stubInputBox.enabled).be.true('Input box should be enabled after target component value updated');
});
});

View File

@@ -4,17 +4,31 @@
*--------------------------------------------------------------------------------------------*/
declare module 'resource-deployment' {
import * as azdata from 'azdata';
import * as vscode from 'vscode';
export const enum ErrorType {
userCancelled,
}
export interface ErrorWithType extends Error {
readonly type: ErrorType;
}
export const enum extension {
name = 'Microsoft.resource-deployment'
}
export interface IOptionsSourceProvider {
readonly optionsSourceId: string,
readonly id: string,
getOptions(): Promise<string[] | azdata.CategoryValue[]> | string[] | azdata.CategoryValue[];
getVariableValue?: (variableName: string, input: string) => Promise<string> | string;
getIsPassword?: (variableName: string) => boolean | Promise<boolean>;
}
export interface IValueProvider {
readonly id: string,
getValue(triggerValue: string): Promise<string>;
}
/**
* Covers defining what the resource-deployment extension exports to other extensions
*
@@ -23,6 +37,7 @@ declare module 'resource-deployment' {
*/
export interface IExtension {
registerOptionsSourceProvider(provider: IOptionsSourceProvider): void
registerOptionsSourceProvider(provider: IOptionsSourceProvider): vscode.Disposable,
registerValueProvider(provider: IValueProvider): vscode.Disposable
}
}

View File

@@ -14,6 +14,7 @@ import { getDateTimeString, getErrorMessage, throwUnless } from '../common/utils
import { AzureAccountFieldInfo, AzureLocationsFieldInfo, ComponentCSSStyles, DialogInfoBase, FieldInfo, FieldType, FilePickerFieldInfo, instanceOfDynamicEnablementInfo, IOptionsSource, KubeClusterContextFieldInfo, LabelPosition, NoteBookEnvironmentVariablePrefix, OptionsInfo, OptionsType, PageInfoBase, RowInfo, SectionInfo, TextCSSStyles } from '../interfaces';
import * as loc from '../localizedConstants';
import { apiService } from '../services/apiService';
import { valueProviderService } from '../services/valueProviderService';
import { getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../services/kubeService';
import { optionsSourcesService } from '../services/optionSourcesService';
import { KubeCtlTool, KubeCtlToolName } from '../services/tools/kubeCtlTool';
@@ -38,6 +39,7 @@ export type InputComponentInfo<T extends InputComponent> = {
component: T;
labelComponent?: azdata.TextComponent;
getValue: () => Promise<InputValueType>;
setValue: (value: InputValueType) => void;
getDisplayValue?: () => Promise<string>;
onValueChanged: vscode.Event<void>;
isPassword?: boolean
@@ -200,6 +202,7 @@ export function createInputBoxInputInfo(view: azdata.ModelView, inputInfo: Input
return {
component: component,
getValue: async (): Promise<InputValueType> => component.value,
setValue: (value: InputValueType) => component.value = value?.toString(),
onValueChanged: component.onTextChanged
};
}
@@ -240,6 +243,7 @@ export function createCheckboxInputInfo(view: azdata.ModelView, info: { initialV
return {
component: checkbox,
getValue: async () => checkbox.checked ? 'true' : 'false',
setValue: (value: InputValueType) => checkbox.checked = value?.toString().toLowerCase() === 'true' ? true : false,
onValueChanged: checkbox.onChanged
};
}
@@ -265,6 +269,7 @@ export function createDropdownInputInfo(view: azdata.ModelView, info: { defaultV
return {
component: dropdown,
getValue: async (): Promise<InputValueType> => typeof dropdown.value === 'string' ? dropdown.value : dropdown.value?.name,
setValue: (value: InputValueType) => setDropdownValue(dropdown, value?.toString()),
getDisplayValue: async (): Promise<string> => (typeof dropdown.value === 'string' ? dropdown.value : dropdown.value?.displayName) || '',
onValueChanged: dropdown.onValueChanged,
};
@@ -331,6 +336,7 @@ export function initializeWizardPage(context: WizardPageContext): void {
});
}));
await hookUpDynamicEnablement(context);
await hookUpValueProviders(context);
const formBuilder = view.modelBuilder.formContainer().withFormItems(
sections.map(section => { return { title: '', component: section }; }),
{
@@ -371,7 +377,8 @@ async function hookUpDynamicEnablement(context: WizardPageContext): Promise<void
}
const updateFields = async () => {
const targetComponentValue = await targetComponent.getValue();
fieldComponent.component.enabled = targetComponentValue === targetValue;
const valuesMatch = targetComponentValue === targetValue;
fieldComponent.component.enabled = valuesMatch;
const isRequired = fieldComponent.component.enabled === false ? false : field.required;
if (fieldComponent.labelComponent) {
fieldComponent.labelComponent.requiredIndicator = isRequired;
@@ -380,6 +387,41 @@ async function hookUpDynamicEnablement(context: WizardPageContext): Promise<void
if ('required' in fieldComponent.component) {
fieldComponent.component.required = isRequired;
}
// When we disable the field then remove the placeholder if it exists so it's clear this field isn't needed
// We only do this for dynamic enablement since if a field is disabled through the JSON directly then it can't
// be modified anyways and so just should not use a placeholder value if they don't want one
if ('placeHolder' in fieldComponent.component) {
fieldComponent.component.placeHolder = valuesMatch ? field.placeHolder : '';
}
};
targetComponent.onValueChanged(() => {
updateFields();
});
await updateFields();
}
}));
}));
}
async function hookUpValueProviders(context: WizardPageContext): Promise<void> {
await Promise.all(context.pageInfo.sections.map(async section => {
if (!section.fields) {
return;
}
await Promise.all(section.fields.map(async field => {
if (field.valueProvider) {
const fieldKey = field.variableName || field.label;
const fieldComponent = context.inputComponents[fieldKey];
const targetComponent = context.inputComponents[field.valueProvider.triggerField];
if (!targetComponent) {
console.error(`Could not find target component ${field.valueProvider.triggerField} when hooking up value providers for ${field.label}`);
return;
}
const provider = valueProviderService.getValueProvider(field.valueProvider.providerId);
const updateFields = async () => {
const targetComponentValue = await targetComponent.getValue();
const newFieldValue = await provider.getValue(targetComponentValue?.toString() ?? '');
fieldComponent.setValue(newFieldValue);
};
targetComponent.onValueChanged(() => {
updateFields();
@@ -572,7 +614,6 @@ async function processOptionsTypeField(context: FieldContext): Promise<void> {
if (context.fieldInfo.options.source?.providerId) {
try {
context.fieldInfo.options.source.provider = optionsSourcesService.getOptionsSource(context.fieldInfo.options.source.providerId);
context.fieldInfo.options.values = await context.fieldInfo.options.source.provider.getOptions();
}
catch (e) {
disableControlButtons(context.container);
@@ -586,16 +627,25 @@ async function processOptionsTypeField(context: FieldContext): Promise<void> {
context.fieldInfo.subFields = context.fieldInfo.subFields || [];
}
let optionsComponent: RadioGroupLoadingComponentBuilder | azdata.DropDownComponent;
const options = context.fieldInfo.options;
const optionsSource = options.source;
if (context.fieldInfo.options.optionsType === OptionsType.Radio) {
optionsComponent = await processRadioOptionsTypeField(context);
let getRadioOptions: (() => Promise<OptionsInfo>) | undefined = undefined;
// If the options are provided for us then set up the callback to load those options async'ly
if (optionsSource?.provider) {
getRadioOptions = async () => {
return { defaultValue: options.defaultValue, values: await optionsSource.provider!.getOptions() };
};
}
optionsComponent = await processRadioOptionsTypeField(context, getRadioOptions);
} else {
throwUnless(context.fieldInfo.options.optionsType === OptionsType.Dropdown, loc.optionsTypeRadioOrDropdown);
optionsComponent = processDropdownOptionsTypeField(context);
}
const optionsSource = context.fieldInfo.options.source;
if (optionsSource?.provider) {
const optionsSourceProvider = optionsSource.provider;
await Promise.all(Object.keys(context.fieldInfo.options.source?.variableNames ?? {}).map(async key => {
await Promise.all(Object.keys(optionsSource?.variableNames ?? {}).map(async key => {
await configureOptionsSourceSubfields(context, optionsSource, key, optionsComponent, optionsSourceProvider);
}));
}
@@ -623,6 +673,7 @@ async function configureOptionsSourceSubfields(context: FieldContext, optionsSou
throw e;
}
},
setValue: (_value: InputValueType) => { throw new Error('Setting value of radio group isn\'t currently supported'); },
onValueChanged: optionsComponent.onValueChanged
});
}
@@ -659,6 +710,7 @@ function processNumberField(context: FieldContext): void {
const value = await input.getValue();
return typeof value === 'string' && value.length > 0 ? parseFloat(value) : value;
},
setValue: (value: InputValueType) => input.component.value = value?.toString(),
onValueChanged: input.onValueChanged
});
}
@@ -755,6 +807,7 @@ function processEvaluatedTextField(context: FieldContext): ReadOnlyFieldInputs {
readOnlyField.text!.value = await substituteVariableValues(context.inputComponents, context.fieldInfo.defaultValue);
return readOnlyField.text!.value;
},
setValue: (value: InputValueType) => readOnlyField.text!.value = value?.toString(),
onValueChanged: onChangedEmitter.event,
});
return readOnlyField;
@@ -921,8 +974,8 @@ async function processKubeConfigClusterPickerField(context: KubeClusterContextFi
}
async function processRadioOptionsTypeField(context: FieldContext): Promise<RadioGroupLoadingComponentBuilder> {
return await createRadioOptions(context);
async function processRadioOptionsTypeField(context: FieldContext, getRadioButtonInfo?: () => Promise<OptionsInfo>): Promise<RadioGroupLoadingComponentBuilder> {
return await createRadioOptions(context, getRadioButtonInfo);
}
@@ -933,18 +986,31 @@ async function createRadioOptions(context: FieldContext, getRadioButtonInfo?: ((
}
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
const radioGroupLoadingComponentBuilder = new RadioGroupLoadingComponentBuilder(context.view, context.onNewDisposableCreated, context.fieldInfo);
context.fieldInfo.labelPosition = LabelPosition.Left;
context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, {
component: radioGroupLoadingComponentBuilder,
labelComponent: label,
getValue: async (): Promise<InputValueType> => radioGroupLoadingComponentBuilder.value,
setValue: (value: InputValueType) => { throw new Error('Setting value of radio group isn\'t currently supported'); },
getDisplayValue: async (): Promise<string> => radioGroupLoadingComponentBuilder.displayValue,
onValueChanged: radioGroupLoadingComponentBuilder.onValueChanged,
});
addLabelInputPairToContainer(context.view, context.components, label, radioGroupLoadingComponentBuilder.component(), context.fieldInfo);
const options = context.fieldInfo.options as OptionsInfo;
await radioGroupLoadingComponentBuilder.loadOptions(
getRadioButtonInfo || options); // wait for the radioGroup to be fully initialized
let loadingText = options?.source?.loadingText;
let loadingCompletedText = options?.source?.loadingCompletedText;
if (loadingText || loadingCompletedText) {
radioGroupLoadingComponentBuilder.withProps({
showText: true,
loadingText: loadingText,
loadingCompletedText: loadingCompletedText
});
}
addLabelInputPairToContainer(context.view, context.components, label, radioGroupLoadingComponentBuilder.component(), context.fieldInfo);
// Start loading the options but continue on so that we can continue setting up the rest of the components - the group
// will show a loading spinner while the options are loaded
radioGroupLoadingComponentBuilder.loadOptions(
getRadioButtonInfo || options).catch(e => console.log('Error loading options for radio group ', e));
return radioGroupLoadingComponentBuilder;
}
@@ -1133,6 +1199,7 @@ function createAzureSubscriptionDropdown(
const inputValue = (await subscriptionDropdown.getValue())?.toString() || '';
return subscriptionValueToSubscriptionMap.get(inputValue)?.id || inputValue;
},
setValue: (value: InputValueType) => setDropdownValue(subscriptionDropdown.component, value?.toString()),
getDisplayValue: subscriptionDropdown.getDisplayValue,
onValueChanged: subscriptionDropdown.onValueChanged
});
@@ -1394,3 +1461,18 @@ export function isInputBoxEmpty(input: azdata.InputBoxComponent): boolean {
return input.value === undefined || input.value === '';
}
/**
* Sets the dropdown value to the corresponding value from the list of current values, converting
* into a CategoryValue if necessary (using the name field).
* @param dropdown The dropdown component to set the value for
* @param value The value to set - either the direct string value or the name of the CategoryValue to use
*/
function setDropdownValue(dropdown: azdata.DropDownComponent, value: string = ''): void {
const values = dropdown.values ?? [];
if (typeof values[0] === 'object') {
dropdown.value = (<azdata.CategoryValue[]>values).find(v => v.name === value);
} else {
dropdown.value = value;
}
}

View File

@@ -12,6 +12,7 @@ import { DeploymentType, NotebookWizardDeploymentProvider, NotebookWizardInfo }
import { IPlatformService } from '../../services/platformService';
import { NotebookWizardAutoSummaryPage } from './notebookWizardAutoSummaryPage';
import { NotebookWizardPage } from './notebookWizardPage';
import { ErrorType, ErrorWithType } from 'resource-deployment';
export class NotebookWizardModel extends ResourceTypeModel {
private _inputComponents: InputComponents = {};
@@ -58,16 +59,27 @@ export class NotebookWizardModel extends ResourceTypeModel {
}
/**
* Generates the notebook and returns true on successful generation
* Generates the notebook and returns true if generation was done and so the wizard should be closed.
**/
public async onGenerateScript(): Promise<boolean> {
const lastPage = this.wizard.lastPage! as NotebookWizardPage;
if (lastPage.validatePage()) {
const notebook = await this.prepareNotebookAndEnvironment();
await this.openNotebook(notebook);
return true;
let notebook: Notebook | undefined;
try {
notebook = await this.prepareNotebookAndEnvironment();
} catch (e) {
const isUserCancelled = e instanceof Error && 'type' in e && (<ErrorWithType>e).type === ErrorType.userCancelled;
// user cancellation is a normal scenario, we just bail out of the wizard without actually opening the notebook, so rethrow for any other case
if (!isUserCancelled) {
throw e;
}
}
if (notebook) { // open the notebook if it was successfully prepared
await this.openNotebook(notebook);
}
return true; // generation done (or cancelled at user request) so close the wizard
} else {
return false;
return false; // validation failed so do not attempt to generate the notebook and do not close the wizard
}
}
@@ -82,7 +94,7 @@ export class NotebookWizardModel extends ResourceTypeModel {
return await this.notebookService.openNotebookWithContent(notebookPath, JSON.stringify(notebook, undefined, 4));
}
private async prepareNotebookAndEnvironment() {
private async prepareNotebookAndEnvironment(): Promise<Notebook> {
await setModelValues(this.inputComponents, this);
const env: NodeJS.ProcessEnv = process.env;
this.setEnvironmentVariables(env, (varName) => {

View File

@@ -2,7 +2,7 @@
"name": "schema-compare",
"displayName": "%displayName%",
"description": "%description%",
"version": "1.8.0",
"version": "1.9.0",
"publisher": "Microsoft",
"preview": false,
"engines": {
@@ -87,13 +87,15 @@
},
"devDependencies": {
"@types/mocha": "^5.2.5",
"@types/sinon": "^9.0.4",
"@types/node": "^12.11.7",
"mocha": "^5.2.0",
"mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7",
"should": "^13.2.1",
"typemoq": "^2.1.0",
"vscodetestcover": "^1.1.0"
"vscodetestcover": "^1.1.0",
"sinon": "^9.0.2"
},
"__metadata": {
"id": "37",

View File

@@ -1,48 +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';
/**
* Wrapper class to act as a facade over VSCode and Data APIs and allow us to test / mock callbacks into
* this API from our code
*/
export class ApiWrapper {
public openConnectionDialog(providers?: string[],
initialConnectionProfile?: azdata.IConnectionProfile,
connectionCompletionOptions?: azdata.IConnectionCompletionOptions): Thenable<azdata.connection.Connection> {
return azdata.connection.openConnectionDialog(providers, initialConnectionProfile, connectionCompletionOptions);
}
public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable {
return vscode.commands.registerCommand(command, callback, thisArg);
}
public getUriForConnection(connectionId: string): Thenable<string> {
return azdata.connection.getUriForConnection(connectionId);
}
public getConnections(activeConnectionsOnly?: boolean): Thenable<azdata.connection.ConnectionProfile[]> {
return azdata.connection.getConnections(activeConnectionsOnly);
}
public connect(connectionProfile: azdata.IConnectionProfile, saveConnection?: boolean, showDashboard?: boolean): Thenable<azdata.ConnectionResult> {
return azdata.connection.connect(connectionProfile, saveConnection, showDashboard);
}
public showErrorMessage(message: string, ...items: string[]): Thenable<string | undefined> {
return vscode.window.showErrorMessage(message, ...items);
}
public showWarningMessage(message: string, options?: vscode.MessageOptions, ...items: string[]): Thenable<string | undefined> {
if (options) {
return vscode.window.showWarningMessage(message, options, ...items);
}
else {
return vscode.window.showWarningMessage(message, ...items);
}
}
}

View File

@@ -4,11 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ApiWrapper } from './common/apiWrapper';
import { SchemaCompareMainWindow } from './schemaCompareMainWindow';
export async function activate(extensionContext: vscode.ExtensionContext): Promise<void> {
vscode.commands.registerCommand('schemaCompare.start', async (context: any) => { await new SchemaCompareMainWindow(new ApiWrapper(), undefined, extensionContext).start(context); });
vscode.commands.registerCommand('schemaCompare.start', async (context: any) => { await new SchemaCompareMainWindow(undefined, extensionContext).start(context); });
}
export function deactivate(): void {

View File

@@ -14,7 +14,6 @@ import { TelemetryReporter, TelemetryViews } from './telemetry';
import { getTelemetryErrorType, getEndpointName, verifyConnectionAndGetOwnerUri, getRootPath } from './utils';
import { SchemaCompareDialog } from './dialogs/schemaCompareDialog';
import { isNullOrUndefined } from 'util';
import { ApiWrapper } from './common/apiWrapper';
// Do not localize this, this is used to decide the icon for the editor.
// TODO : In future icon should be decided based on language id (scmp) and not resource name
@@ -51,8 +50,8 @@ export class SchemaCompareMainWindow {
private SchemaCompareActionMap: Map<Number, string>;
private operationId: string;
protected comparisonResult: mssql.SchemaCompareResult;
private sourceNameComponent: azdata.TableComponent;
private targetNameComponent: azdata.TableComponent;
private sourceNameComponent: azdata.InputBoxComponent;
private targetNameComponent: azdata.InputBoxComponent;
private deploymentOptions: mssql.DeploymentOptions;
private schemaCompareOptionDialog: SchemaCompareOptionsDialog;
private tablelistenersToDispose: vscode.Disposable[] = [];
@@ -69,7 +68,7 @@ export class SchemaCompareMainWindow {
public sourceEndpointInfo: mssql.SchemaCompareEndpointInfo;
public targetEndpointInfo: mssql.SchemaCompareEndpointInfo;
constructor(private apiWrapper: ApiWrapper, private schemaCompareService?: mssql.ISchemaCompareService, private extensionContext?: vscode.ExtensionContext) {
constructor(private schemaCompareService?: mssql.ISchemaCompareService, private extensionContext?: vscode.ExtensionContext) {
this.SchemaCompareActionMap = new Map<Number, string>();
this.SchemaCompareActionMap[mssql.SchemaUpdateAction.Delete] = loc.deleteAction;
this.SchemaCompareActionMap[mssql.SchemaUpdateAction.Change] = loc.changeAction;
@@ -87,7 +86,7 @@ export class SchemaCompareMainWindow {
let profile = context ? <azdata.IConnectionProfile>context.connectionProfile : undefined;
let sourceDacpac = context as string;
if (profile) {
let ownerUri = await this.apiWrapper.getUriForConnection((profile.id));
let ownerUri = await azdata.connection.getUriForConnection((profile.id));
this.sourceEndpointInfo = {
endpointType: mssql.SchemaCompareEndpointType.Database,
serverDisplayName: `${profile.serverName} ${profile.userName}`,
@@ -157,26 +156,16 @@ export class SchemaCompareMainWindow {
this.sourceName = getEndpointName(this.sourceEndpointInfo);
this.targetName = ' ';
this.sourceNameComponent = view.modelBuilder.table().withProperties<azdata.TableComponentProperties>({
data: [],
columns: [
{
value: this.sourceName,
headerCssClass: 'no-borders',
toolTip: this.sourceName
},
]
this.sourceNameComponent = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
value: this.sourceName,
title: this.sourceName,
enabled: false
}).component();
this.targetNameComponent = view.modelBuilder.table().withProperties<azdata.TableComponentProperties>({
data: [],
columns: [
{
value: this.targetName,
headerCssClass: 'no-borders',
toolTip: this.targetName
},
]
this.targetNameComponent = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
value: this.targetName,
title: this.targetName,
enabled: false
}).component();
this.resetButtons(ResetButtonState.noSourceTarget);
@@ -218,10 +207,10 @@ export class SchemaCompareMainWindow {
sourceTargetLabels.addItem(sourceLabel, { CSSStyles: { 'width': '55%', 'margin-left': '15px', 'font-size': 'larger', 'font-weight': 'bold' } });
sourceTargetLabels.addItem(targetLabel, { CSSStyles: { 'width': '45%', 'font-size': 'larger', 'font-weight': 'bold' } });
this.sourceTargetFlexLayout.addItem(this.sourceNameComponent, { CSSStyles: { 'width': '40%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px' } });
this.sourceTargetFlexLayout.addItem(this.sourceNameComponent, { CSSStyles: { 'width': '40%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px', 'margin-right': '10px' } });
this.sourceTargetFlexLayout.addItem(this.selectSourceButton, { CSSStyles: { 'margin-top': '10px' } });
this.sourceTargetFlexLayout.addItem(arrowLabel, { CSSStyles: { 'width': '10%', 'font-size': 'larger', 'text-align-last': 'center' } });
this.sourceTargetFlexLayout.addItem(this.targetNameComponent, { CSSStyles: { 'width': '40%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px' } });
this.sourceTargetFlexLayout.addItem(this.targetNameComponent, { CSSStyles: { 'width': '40%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px', 'margin-right': '10px' } });
this.sourceTargetFlexLayout.addItem(this.selectTargetButton, { CSSStyles: { 'margin-top': '10px' } });
this.loader = view.modelBuilder.loadingComponent().component();
@@ -259,21 +248,14 @@ export class SchemaCompareMainWindow {
this.sourceName = getEndpointName(this.sourceEndpointInfo);
this.targetName = getEndpointName(this.targetEndpointInfo);
this.sourceNameComponent.updateProperty('columns', [
{
value: this.sourceName,
headerCssClass: 'no-borders',
toolTip: this.sourceName
},
]);
this.targetNameComponent.updateProperty('columns', [
{
value: this.targetName,
headerCssClass: 'no-borders',
toolTip: this.targetName
},
]);
this.sourceNameComponent.updateProperties({
value: this.sourceName,
title: this.sourceName
});
this.targetNameComponent.updateProperties({
value: this.targetName,
title: this.targetName
});
if (!this.sourceName || !this.targetName || this.sourceName === ' ' || this.targetName === ' ') {
this.resetButtons(ResetButtonState.noSourceTarget);
} else {
@@ -295,11 +277,11 @@ export class SchemaCompareMainWindow {
}
this.comparisonResult = await service.schemaCompare(this.operationId, this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions);
if (!this.comparisonResult || !this.comparisonResult.success) {
TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaComparisonFailed', undefined, getTelemetryErrorType(this.comparisonResult.errorMessage))
TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaComparisonFailed', undefined, getTelemetryErrorType(this.comparisonResult?.errorMessage))
.withAdditionalProperties({
operationId: this.comparisonResult.operationId
}).send();
this.apiWrapper.showErrorMessage(loc.compareErrorMessage(this.comparisonResult.errorMessage));
vscode.window.showErrorMessage(loc.compareErrorMessage(this.comparisonResult?.errorMessage));
return;
}
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaComparisonFinished')
@@ -407,69 +389,76 @@ export class SchemaCompareMainWindow {
this.tablelistenersToDispose.push(this.differencesTable.onCellAction(async (rowState) => {
let checkboxState = <azdata.ICheckboxCellActionEventArgs>rowState;
if (checkboxState) {
// show an info notification the first time when trying to exclude to notify the user that it may take some time to calculate affected dependencies
if (this.showIncludeExcludeWaitingMessage) {
this.showIncludeExcludeWaitingMessage = false;
vscode.window.showInformationMessage(loc.includeExcludeInfoMessage);
}
let diff = this.comparisonResult.differences[checkboxState.row];
const result = await service.schemaCompareIncludeExcludeNode(this.comparisonResult.operationId, diff, checkboxState.checked, azdata.TaskExecutionMode.execute);
let checkboxesToChange = [];
if (result.success) {
this.saveExcludeState(checkboxState);
// dependencies could have been included or excluded as a result, so save their exclude states
result.affectedDependencies.forEach(difference => {
// find the row of the difference and set its checkbox
const diffEntryKey = this.createDiffEntryKey(difference);
if (this.diffEntryRowMap.has(diffEntryKey)) {
const row = this.diffEntryRowMap.get(diffEntryKey);
checkboxesToChange.push({ row: row, column: 2, columnName: 'Include', checked: difference.included });
const dependencyCheckBoxState: azdata.ICheckboxCellActionEventArgs = {
checked: difference.included,
row: row,
column: 2,
columnName: undefined
};
this.saveExcludeState(dependencyCheckBoxState);
}
});
} else {
// failed because of dependencies
if (result.blockingDependencies) {
// show the first dependent that caused this to fail in the warning message
const diffEntryName = this.createName(diff.sourceValue ? diff.sourceValue : diff.targetValue);
const firstDependentName = this.createName(result.blockingDependencies[0].sourceValue ? result.blockingDependencies[0].sourceValue : result.blockingDependencies[0].targetValue);
let cannotExcludeMessage: string;
let cannotIncludeMessage: string;
if (firstDependentName) {
cannotExcludeMessage = loc.cannotExcludeMessageDependent(diffEntryName, firstDependentName);
cannotIncludeMessage = loc.cannotIncludeMessageDependent(diffEntryName, firstDependentName);
} else {
cannotExcludeMessage = loc.cannotExcludeMessage(diffEntryName);
cannotIncludeMessage = loc.cannotIncludeMessage(diffEntryName);
}
vscode.window.showWarningMessage(checkboxState.checked ? cannotIncludeMessage : cannotExcludeMessage);
} else {
vscode.window.showWarningMessage(result.errorMessage);
}
// set checkbox back to previous state
checkboxesToChange.push({ row: checkboxState.row, column: checkboxState.column, columnName: 'Include', checked: !checkboxState.checked });
}
if (checkboxesToChange.length > 0) {
this.differencesTable.updateCells = checkboxesToChange;
}
await this.applyIncludeExclude(checkboxState);
}
}));
}
public async applyIncludeExclude(checkboxState: azdata.ICheckboxCellActionEventArgs): Promise<void> {
const service = await this.getService();
// show an info notification the first time when trying to exclude to notify the user that it may take some time to calculate affected dependencies
if (this.showIncludeExcludeWaitingMessage) {
this.showIncludeExcludeWaitingMessage = false;
vscode.window.showInformationMessage(loc.includeExcludeInfoMessage);
}
let diff = this.comparisonResult.differences[checkboxState.row];
const result = await service.schemaCompareIncludeExcludeNode(this.comparisonResult.operationId, diff, checkboxState.checked, azdata.TaskExecutionMode.execute);
let checkboxesToChange = [];
if (result.success) {
this.saveExcludeState(checkboxState);
// dependencies could have been included or excluded as a result, so save their exclude states
result.affectedDependencies.forEach(difference => {
// find the row of the difference and set its checkbox
const diffEntryKey = this.createDiffEntryKey(difference);
if (this.diffEntryRowMap.has(diffEntryKey)) {
const row = this.diffEntryRowMap.get(diffEntryKey);
checkboxesToChange.push({ row: row, column: 2, columnName: 'Include', checked: difference.included });
const dependencyCheckBoxState: azdata.ICheckboxCellActionEventArgs = {
checked: difference.included,
row: row,
column: 2,
columnName: undefined
};
this.saveExcludeState(dependencyCheckBoxState);
}
});
} else {
// failed because of dependencies
if (result.blockingDependencies) {
// show the first dependent that caused this to fail in the warning message
const diffEntryName = this.createName(diff.sourceValue ? diff.sourceValue : diff.targetValue);
const firstDependentName = this.createName(result.blockingDependencies[0].sourceValue ? result.blockingDependencies[0].sourceValue : result.blockingDependencies[0].targetValue);
let cannotExcludeMessage: string;
let cannotIncludeMessage: string;
if (firstDependentName) {
cannotExcludeMessage = loc.cannotExcludeMessageDependent(diffEntryName, firstDependentName);
cannotIncludeMessage = loc.cannotIncludeMessageDependent(diffEntryName, firstDependentName);
} else {
cannotExcludeMessage = loc.cannotExcludeMessage(diffEntryName);
cannotIncludeMessage = loc.cannotIncludeMessage(diffEntryName);
}
vscode.window.showWarningMessage(checkboxState.checked ? cannotIncludeMessage : cannotExcludeMessage);
} else {
vscode.window.showWarningMessage(result.errorMessage);
}
// set checkbox back to previous state
checkboxesToChange.push({ row: checkboxState.row, column: checkboxState.column, columnName: 'Include', checked: !checkboxState.checked });
}
if (checkboxesToChange.length > 0) {
this.differencesTable.updateCells = checkboxesToChange;
}
}
// save state based on source name if present otherwise target name (parity with SSDT)
private saveExcludeState(rowState: azdata.ICheckboxCellActionEventArgs) {
if (rowState) {
this.differencesTable.data[rowState.row][2] = rowState.checked;
if (this.differencesTable.data[rowState.row]?.length > 2) {
this.differencesTable.data[rowState.row][2] = rowState.checked;
}
let diff = this.comparisonResult.differences[rowState.row];
let key = (diff.sourceValue && diff.sourceValue.length > 0) ? this.createName(diff.sourceValue) : this.createName(diff.targetValue);
if (key) {
@@ -647,7 +636,7 @@ export class SchemaCompareMainWindow {
});
}
private async cancelCompare() {
public async cancelCompare() {
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareCancelStarted')
.withAdditionalProperties({
@@ -691,28 +680,32 @@ export class SchemaCompareMainWindow {
}).component();
this.generateScriptButton.onDidClick(async (click) => {
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareGenerateScriptStarted')
.withAdditionalProperties({
'startTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
}).send();
const service = await this.getService();
const result = await service.schemaCompareGenerateScript(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.script);
if (!result || !result.success) {
TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareGenerateScriptFailed', undefined, getTelemetryErrorType(result.errorMessage))
.withAdditionalProperties({
'operationId': this.comparisonResult.operationId
}).send();
vscode.window.showErrorMessage(loc.generateScriptErrorMessage(result.errorMessage));
}
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareGenerateScriptEnded')
.withAdditionalProperties({
'endTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
}).send();
await this.generateScript();
});
}
public async generateScript(): Promise<void> {
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareGenerateScriptStarted')
.withAdditionalProperties({
'startTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
}).send();
const service = await this.getService();
const result = await service.schemaCompareGenerateScript(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.script);
if (!result || !result.success) {
TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareGenerateScriptFailed', undefined, getTelemetryErrorType(result.errorMessage))
.withAdditionalProperties({
'operationId': this.comparisonResult.operationId
}).send();
vscode.window.showErrorMessage(loc.generateScriptErrorMessage(result.errorMessage));
}
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareGenerateScriptEnded')
.withAdditionalProperties({
'endTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
}).send();
}
private createOptionsButton(view: azdata.ModelView) {
this.optionsButton = view.modelBuilder.button().withProperties({
label: loc.options,
@@ -741,43 +734,47 @@ export class SchemaCompareMainWindow {
},
}).component();
this.applyButton.onDidClick(async (click) => {
await this.publishChanges();
});
}
public async publishChanges(): Promise<void> {
// need only yes button - since the modal dialog has a default cancel
const yesString = loc.YesButtonText;
this.applyButton.onDidClick(async (click) => {
await vscode.window.showWarningMessage(loc.applyConfirmation, { modal: true }, yesString).then(async (result) => {
if (result === yesString) {
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyStarted')
.withAdditionalProperties({
'startTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
}).send();
vscode.window.showWarningMessage(loc.applyConfirmation, { modal: true }, yesString).then(async (result) => {
if (result === yesString) {
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyStarted')
// disable apply and generate script buttons because the results are no longer valid after applying the changes
this.setButtonsForRecompare();
const service = await this.getService();
const result = await service.schemaComparePublishChanges(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.execute);
if (!result || !result.success) {
TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyFailed', undefined, getTelemetryErrorType(result.errorMessage))
.withAdditionalProperties({
'startTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
}).send();
vscode.window.showErrorMessage(loc.applyErrorMessage(result.errorMessage));
// disable apply and generate script buttons because the results are no longer valid after applying the changes
this.setButtonsForRecompare();
const service = await this.getService();
const result = await service.schemaComparePublishChanges(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.execute);
if (!result || !result.success) {
TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyFailed', undefined, getTelemetryErrorType(result.errorMessage))
.withAdditionalProperties({
'operationId': this.comparisonResult.operationId
}).send();
vscode.window.showErrorMessage(loc.applyErrorMessage(result.errorMessage));
// reenable generate script and apply buttons if apply failed
this.generateScriptButton.enabled = true;
this.generateScriptButton.title = loc.generateScriptEnabledMessage;
this.applyButton.enabled = true;
this.applyButton.title = loc.applyEnabledMessage;
}
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyEnded')
.withAdditionalProperties({
'endTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
}).send();
// reenable generate script and apply buttons if apply failed
this.generateScriptButton.enabled = true;
this.generateScriptButton.title = loc.generateScriptEnabledMessage;
this.applyButton.enabled = true;
this.applyButton.title = loc.applyEnabledMessage;
}
});
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareApplyEnded')
.withAdditionalProperties({
'endTime': Date.now().toString(),
'operationId': this.comparisonResult.operationId
}).send();
}
});
}
@@ -856,23 +853,13 @@ export class SchemaCompareMainWindow {
[this.sourceName, this.targetName] = [this.targetName, this.sourceName];
this.sourceNameComponent.updateProperties({
columns: [
{
value: this.sourceName,
headerCssClass: 'no-borders',
toolTip: this.sourceName
},
]
value: this.sourceName,
title: this.sourceName
});
this.targetNameComponent.updateProperties({
columns: [
{
value: this.targetName,
headerCssClass: 'no-borders',
toolTip: this.targetName
},
]
value: this.targetName,
title: this.targetName
});
// remember that source target have been toggled
@@ -922,60 +909,64 @@ export class SchemaCompareMainWindow {
}).component();
this.openScmpButton.onDidClick(async (click) => {
TelemetryReporter.sendActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareOpenScmpStarted');
const rootPath = getRootPath();
let fileUris = await vscode.window.showOpenDialog(
{
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: vscode.Uri.file(rootPath),
openLabel: loc.open,
filters: {
'scmp Files': ['scmp'],
}
}
);
if (!fileUris || fileUris.length === 0) {
return;
}
let fileUri = fileUris[0];
const service = await this.getService();
let startTime = Date.now();
const result = await service.schemaCompareOpenScmp(fileUri.fsPath);
if (!result || !result.success) {
TelemetryReporter.sendErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareOpenScmpFailed', undefined, getTelemetryErrorType(result.errorMessage));
vscode.window.showErrorMessage(loc.openScmpErrorMessage(result.errorMessage));
return;
}
this.sourceEndpointInfo = await this.constructEndpointInfo(result.sourceEndpointInfo, loc.sourceTitle, this.apiWrapper);
this.targetEndpointInfo = await this.constructEndpointInfo(result.targetEndpointInfo, loc.targetTitle, this.apiWrapper);
this.updateSourceAndTarget();
this.setDeploymentOptions(result.deploymentOptions);
this.scmpSourceExcludes = result.excludedSourceElements;
this.scmpTargetExcludes = result.excludedTargetElements;
this.sourceTargetSwitched = result.originalTargetName !== this.targetEndpointInfo.databaseName;
// clear out any old results
this.resetForNewCompare();
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareOpenScmpEnded')
.withAdditionalProperties({
elapsedTime: (Date.now() - startTime).toString()
}).send();
await this.openScmp();
});
}
private async constructEndpointInfo(endpoint: mssql.SchemaCompareEndpointInfo, caller: string, apiWrapper: ApiWrapper): Promise<mssql.SchemaCompareEndpointInfo> {
public async openScmp(): Promise<void> {
TelemetryReporter.sendActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareOpenScmpStarted');
const rootPath = getRootPath();
let fileUris = await vscode.window.showOpenDialog(
{
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: vscode.Uri.file(rootPath),
openLabel: loc.open,
filters: {
'scmp Files': ['scmp'],
}
}
);
if (!fileUris || fileUris.length === 0) {
return;
}
let fileUri = fileUris[0];
const service = await this.getService();
let startTime = Date.now();
const result = await service.schemaCompareOpenScmp(fileUri.fsPath);
if (!result || !result.success) {
TelemetryReporter.sendErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareOpenScmpFailed', undefined, getTelemetryErrorType(result.errorMessage));
vscode.window.showErrorMessage(loc.openScmpErrorMessage(result.errorMessage));
return;
}
this.sourceEndpointInfo = await this.constructEndpointInfo(result.sourceEndpointInfo, loc.sourceTitle);
this.targetEndpointInfo = await this.constructEndpointInfo(result.targetEndpointInfo, loc.targetTitle);
this.updateSourceAndTarget();
this.setDeploymentOptions(result.deploymentOptions);
this.scmpSourceExcludes = result.excludedSourceElements;
this.scmpTargetExcludes = result.excludedTargetElements;
this.sourceTargetSwitched = result.originalTargetName !== this.targetEndpointInfo.databaseName;
// clear out any old results
this.resetForNewCompare();
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareOpenScmpEnded')
.withAdditionalProperties({
elapsedTime: (Date.now() - startTime).toString()
}).send();
}
private async constructEndpointInfo(endpoint: mssql.SchemaCompareEndpointInfo, caller: string): Promise<mssql.SchemaCompareEndpointInfo> {
let ownerUri;
let endpointInfo;
if (endpoint && endpoint.endpointType === mssql.SchemaCompareEndpointType.Database) {
// only set endpoint info if able to connect to the database
ownerUri = await verifyConnectionAndGetOwnerUri(endpoint, caller, apiWrapper);
ownerUri = await verifyConnectionAndGetOwnerUri(endpoint, caller);
}
if (ownerUri) {
endpointInfo = endpoint;
@@ -1007,44 +998,48 @@ export class SchemaCompareMainWindow {
}).component();
this.saveScmpButton.onDidClick(async (click) => {
const rootPath = getRootPath();
const filePath = await vscode.window.showSaveDialog(
{
defaultUri: vscode.Uri.file(rootPath),
saveLabel: loc.save,
filters: {
'scmp Files': ['scmp'],
}
}
);
if (!filePath) {
return;
}
// convert include/exclude maps to arrays of object ids
let sourceExcludes: mssql.SchemaCompareObjectId[] = this.convertExcludesToObjectIds(this.originalSourceExcludes);
let targetExcludes: mssql.SchemaCompareObjectId[] = this.convertExcludesToObjectIds(this.originalTargetExcludes);
let startTime = Date.now();
TelemetryReporter.sendActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSaveScmp');
const service = await this.getService();
const result = await service.schemaCompareSaveScmp(this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions, filePath.fsPath, sourceExcludes, targetExcludes);
if (!result || !result.success) {
TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSaveScmpFailed', undefined, getTelemetryErrorType(result.errorMessage))
.withAdditionalProperties({
operationId: this.comparisonResult.operationId
}).send();
vscode.window.showErrorMessage(loc.saveScmpErrorMessage(result.errorMessage));
}
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSaveScmpEnded')
.withAdditionalProperties({
elapsedTime: (Date.now() - startTime).toString(),
operationId: this.comparisonResult.operationId
});
await this.saveScmp();
});
}
public async saveScmp(): Promise<void> {
const rootPath = getRootPath();
const filePath = await vscode.window.showSaveDialog(
{
defaultUri: vscode.Uri.file(rootPath),
saveLabel: loc.save,
filters: {
'scmp Files': ['scmp'],
}
}
);
if (!filePath) {
return;
}
// convert include/exclude maps to arrays of object ids
let sourceExcludes: mssql.SchemaCompareObjectId[] = this.convertExcludesToObjectIds(this.originalSourceExcludes);
let targetExcludes: mssql.SchemaCompareObjectId[] = this.convertExcludesToObjectIds(this.originalTargetExcludes);
let startTime = Date.now();
TelemetryReporter.sendActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSaveScmp');
const service = await this.getService();
const result = await service.schemaCompareSaveScmp(this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions, filePath.fsPath, sourceExcludes, targetExcludes);
if (!result || !result.success) {
TelemetryReporter.createErrorEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSaveScmpFailed', undefined, getTelemetryErrorType(result.errorMessage))
.withAdditionalProperties({
operationId: this.comparisonResult.operationId
}).send();
vscode.window.showErrorMessage(loc.saveScmpErrorMessage(result.errorMessage));
}
TelemetryReporter.createActionEvent(TelemetryViews.SchemaCompareMainWindow, 'SchemaCompareSaveScmpEnded')
.withAdditionalProperties({
elapsedTime: (Date.now() - startTime).toString(),
operationId: this.comparisonResult.operationId
});
}
/**
* Converts excluded diff entries into object ids which are needed to save them in an scmp
*/
@@ -1071,7 +1066,7 @@ export class SchemaCompareMainWindow {
}
private async getService(): Promise<mssql.ISchemaCompareService> {
if (isNullOrUndefined(this.schemaCompareService)) {
if (this.schemaCompareService === null || this.schemaCompareService === undefined) {
this.schemaCompareService = (vscode.extensions.getExtension(mssql.extension.name).exports as mssql.IExtension).schemaCompare;
}
return this.schemaCompareService;

View File

@@ -4,13 +4,12 @@
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as mssql from '../../../mssql';
import * as TypeMoq from 'typemoq';
import * as loc from '../localizedConstants';
import 'mocha';
import { SchemaCompareDialog } from './../dialogs/schemaCompareDialog';
import * as sinon from 'sinon';
import { SchemaCompareMainWindow } from '../schemaCompareMainWindow';
import { SchemaCompareTestService, testStateScmp } from './testSchemaCompareService';
import { createContext, TestContext } from './testContext';
@@ -28,15 +27,20 @@ before(function (): void {
testContext = createContext();
});
describe('SchemaCompareMainWindow.start', function (): void {
describe('SchemaCompareMainWindow.start @DacFx@', function (): void {
before(() => {
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
mockExtensionContext.setup(x => x.extensionPath).returns(() => '');
});
this.afterEach(() => {
sinon.restore();
});
it('Should be correct when created.', async function (): Promise<void> {
let sc = new SchemaCompareTestService();
let result = new SchemaCompareMainWindowTest(testContext.apiWrapper.object, sc, mockExtensionContext.object);
let result = new SchemaCompareMainWindowTest(sc, mockExtensionContext.object);
await result.start(undefined);
should(result.getComparisonResult() === undefined);
@@ -52,7 +56,7 @@ describe('SchemaCompareMainWindow.start', function (): void {
it('Should start with the source as undefined', async function (): Promise<void> {
let sc = new SchemaCompareTestService();
let result = new SchemaCompareMainWindowTest(testContext.apiWrapper.object, sc, mockExtensionContext.object);
let result = new SchemaCompareMainWindowTest(sc, mockExtensionContext.object);
await result.start(undefined);
should.equal(result.sourceEndpointInfo, undefined);
@@ -62,8 +66,8 @@ describe('SchemaCompareMainWindow.start', function (): void {
it('Should start with the source as database', async function (): Promise<void> {
let sc = new SchemaCompareTestService();
let result = new SchemaCompareMainWindowTest(testContext.apiWrapper.object, sc, mockExtensionContext.object);
await result.start({connectionProfile: mockIConnectionProfile});
let result = new SchemaCompareMainWindowTest(sc, mockExtensionContext.object);
await result.start({ connectionProfile: mockIConnectionProfile });
should.notEqual(result.sourceEndpointInfo, undefined);
should.equal(result.sourceEndpointInfo.endpointType, mssql.SchemaCompareEndpointType.Database);
@@ -75,7 +79,7 @@ describe('SchemaCompareMainWindow.start', function (): void {
it('Should start with the source as dacpac.', async function (): Promise<void> {
let sc = new SchemaCompareTestService();
let result = new SchemaCompareMainWindowTest(testContext.apiWrapper.object, sc, mockExtensionContext.object);
let result = new SchemaCompareMainWindowTest(sc, mockExtensionContext.object);
const dacpacPath = mockFilePath;
await result.start(dacpacPath);
@@ -85,7 +89,287 @@ describe('SchemaCompareMainWindow.start', function (): void {
should.equal(result.targetEndpointInfo, undefined);
});
});
let showErrorMessageSpy: any;
let showWarningMessageStub: any;
let showOpenDialogStub: any;
describe('SchemaCompareMainWindow.results', function (): void {
before(() => {
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
mockExtensionContext.setup(x => x.extensionPath).returns(() => '');
});
this.afterEach(() => {
sinon.restore();
});
this.beforeEach(() => {
sinon.restore();
showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage');
});
it('Should show error if publish changes fails', async function (): Promise<void> {
let service = createServiceMock();
service.setup(x => x.schemaComparePublishChanges(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({
success: false,
errorMessage: 'error1'
}));
showWarningMessageStub = sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve('Yes'));
let schemaCompareResult = new SchemaCompareMainWindow(service.object, mockExtensionContext.object);
await schemaCompareResult.start(undefined);
schemaCompareResult.sourceEndpointInfo = setDacpacEndpointInfo(mocksource);
schemaCompareResult.targetEndpointInfo = setDacpacEndpointInfo(mocktarget);
await schemaCompareResult.execute();
await schemaCompareResult.publishChanges();
should(showErrorMessageSpy.calledOnce).be.true();
should.equal(showErrorMessageSpy.getCall(0).args[0], loc.applyErrorMessage('error1'));
});
it('Should show not error if publish changes succeed', async function (): Promise<void> {
let service = createServiceMock();
service.setup(x => x.schemaComparePublishChanges(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({
success: true,
errorMessage: ''
}));
showWarningMessageStub = sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve('Yes'));
let schemaCompareResult = new SchemaCompareMainWindow(service.object, mockExtensionContext.object);
await schemaCompareResult.start(undefined);
schemaCompareResult.sourceEndpointInfo = setDacpacEndpointInfo(mocksource);
schemaCompareResult.targetEndpointInfo = setDacpacEndpointInfo(mocktarget);
await schemaCompareResult.execute();
await schemaCompareResult.publishChanges();
should(showErrorMessageSpy.notCalled).be.true();
});
it('Should show error if openScmp fails', async function (): Promise<void> {
let service = createServiceMock();
let files: vscode.Uri[] = [vscode.Uri.parse('file:///test')];
service.setup(x => x.schemaCompareOpenScmp(TypeMoq.It.isAny())).returns(() => Promise.resolve({
sourceEndpointInfo: undefined,
targetEndpointInfo: undefined,
originalTargetName: 'string',
originalConnectionString: '',
deploymentOptions: undefined,
excludedSourceElements: [],
excludedTargetElements: [],
success: false,
errorMessage: 'error1'
}));
showWarningMessageStub = sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve('Yes'));
showOpenDialogStub = sinon.stub(vscode.window, 'showOpenDialog').returns(<any>Promise.resolve(files));
let schemaCompareResult = new SchemaCompareMainWindow(service.object, mockExtensionContext.object);
await schemaCompareResult.start(undefined);
schemaCompareResult.sourceEndpointInfo = setDacpacEndpointInfo(mocksource);
schemaCompareResult.targetEndpointInfo = setDacpacEndpointInfo(mocktarget);
await schemaCompareResult.openScmp();
should(showErrorMessageSpy.calledOnce).be.true();
should.equal(showErrorMessageSpy.getCall(0).args[0], loc.openScmpErrorMessage('error1'));
});
it('Should show error if saveScmp fails', async function (): Promise<void> {
let service = createServiceMock();
let file: vscode.Uri = vscode.Uri.parse('file:///test');
service.setup(x => x.schemaCompareSaveScmp(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({
sourceEndpointInfo: undefined,
targetEndpointInfo: undefined,
originalTargetName: 'string',
originalConnectionString: '',
deploymentOptions: undefined,
excludedSourceElements: [],
excludedTargetElements: [],
success: false,
errorMessage: 'error1'
}));
showWarningMessageStub = sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve('Yes'));
showOpenDialogStub = sinon.stub(vscode.window, 'showSaveDialog').returns(<any>Promise.resolve(file));
let schemaCompareResult = new SchemaCompareMainWindow(service.object, mockExtensionContext.object);
await schemaCompareResult.start(undefined);
schemaCompareResult.sourceEndpointInfo = setDacpacEndpointInfo(mocksource);
schemaCompareResult.targetEndpointInfo = setDacpacEndpointInfo(mocktarget);
await schemaCompareResult.execute();
await schemaCompareResult.saveScmp();
should(showErrorMessageSpy.calledOnce).be.true();
should.equal(showErrorMessageSpy.getCall(0).args[0], loc.saveScmpErrorMessage('error1'));
});
it('Should show error if generateScript fails', async function (): Promise<void> {
let service = createServiceMock();
service.setup(x => x.schemaCompareGenerateScript(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({
success: false,
errorMessage: 'error1'
}));
showWarningMessageStub = sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve('Yes'));
let schemaCompareResult = new SchemaCompareMainWindow(service.object, mockExtensionContext.object);
await schemaCompareResult.start(undefined);
schemaCompareResult.sourceEndpointInfo = setDacpacEndpointInfo(mocksource);
schemaCompareResult.targetEndpointInfo = setDacpacEndpointInfo(mocktarget);
await schemaCompareResult.execute();
await schemaCompareResult.generateScript();
should(showErrorMessageSpy.calledOnce).be.true();
should.equal(showErrorMessageSpy.getCall(0).args[0], loc.generateScriptErrorMessage('error1'));
});
it('Should show error if cancel fails', async function (): Promise<void> {
let service = createServiceMock();
service.setup(x => x.schemaCompareCancel(TypeMoq.It.isAny())).returns(() => Promise.resolve({
success: false,
errorMessage: 'error1'
}));
showWarningMessageStub = sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve('Yes'));
let schemaCompareResult = new SchemaCompareMainWindow(service.object, mockExtensionContext.object);
await schemaCompareResult.start(undefined);
schemaCompareResult.sourceEndpointInfo = setDacpacEndpointInfo(mocksource);
schemaCompareResult.targetEndpointInfo = setDacpacEndpointInfo(mocktarget);
await schemaCompareResult.execute();
await schemaCompareResult.cancelCompare();
should(showErrorMessageSpy.calledOnce).be.true();
should.equal(showErrorMessageSpy.getCall(0).args[0], loc.cancelErrorMessage('error1'));
});
it('Should show error if IncludeExcludeNode fails', async function (): Promise<void> {
let service = createServiceMock();
service.setup(x => x.schemaCompareIncludeExcludeNode(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({
success: false,
errorMessage: '',
affectedDependencies: [],
blockingDependencies: [{
updateAction: 2,
differenceType: 0,
name: 'SqlTable',
sourceValue: ['dbo', 'table1'],
targetValue: null,
parent: null,
children: [{
updateAction: 2,
differenceType: 0,
name: 'SqlSimpleColumn',
sourceValue: ['dbo', 'table1', 'id'],
targetValue: null,
parent: null,
children: [],
sourceScript: '',
targetScript: null,
included: false
}],
sourceScript: 'CREATE TABLE [dbo].[table1](id int)',
targetScript: null,
included: true
}]
}));
showWarningMessageStub = sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve(''));
let schemaCompareResult = new SchemaCompareMainWindow(service.object, mockExtensionContext.object);
await schemaCompareResult.start(undefined);
schemaCompareResult.sourceEndpointInfo = setDacpacEndpointInfo(mocksource);
schemaCompareResult.targetEndpointInfo = setDacpacEndpointInfo(mocktarget);
await schemaCompareResult.execute();
await schemaCompareResult.applyIncludeExclude({
row: 0,
column: 0,
columnName: 1,
checked: true
});
should(showWarningMessageStub.calledOnce).be.true();
});
it('Should not show warning if IncludeExcludeNode succeed', async function (): Promise<void> {
let service = createServiceMock();
service.setup(x => x.schemaCompareIncludeExcludeNode(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({
success: true,
errorMessage: '',
affectedDependencies: [],
blockingDependencies: [{
updateAction: 2,
differenceType: 0,
name: 'SqlTable',
sourceValue: ['dbo', 'table1'],
targetValue: null,
parent: null,
children: [{
updateAction: 2,
differenceType: 0,
name: 'SqlSimpleColumn',
sourceValue: ['dbo', 'table1', 'id'],
targetValue: null,
parent: null,
children: [],
sourceScript: '',
targetScript: null,
included: false
}],
sourceScript: 'CREATE TABLE [dbo].[table1](id int)',
targetScript: null,
included: true
}]
}));
showWarningMessageStub = sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve(''));
let schemaCompareResult = new SchemaCompareMainWindow(service.object, mockExtensionContext.object);
await schemaCompareResult.start(undefined);
schemaCompareResult.sourceEndpointInfo = setDacpacEndpointInfo(mocksource);
schemaCompareResult.targetEndpointInfo = setDacpacEndpointInfo(mocktarget);
await schemaCompareResult.execute();
await schemaCompareResult.applyIncludeExclude({
row: 0,
column: 0,
columnName: 1,
checked: false
});
should(showWarningMessageStub.notCalled).be.true();
});
it('Should not show error if user does not want to publish', async function (): Promise<void> {
let service = createServiceMock();
service.setup(x => x.schemaComparePublishChanges(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({
success: true,
errorMessage: ''
}));
showWarningMessageStub = sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve('No'));
let schemaCompareResult = new SchemaCompareMainWindow(service.object, mockExtensionContext.object);
await schemaCompareResult.start(undefined);
schemaCompareResult.sourceEndpointInfo = setDacpacEndpointInfo(mocksource);
schemaCompareResult.targetEndpointInfo = setDacpacEndpointInfo(mocktarget);
await schemaCompareResult.execute();
await schemaCompareResult.publishChanges();
should(showErrorMessageSpy.notCalled).be.true();
});
function createServiceMock() {
let sc = new SchemaCompareTestService(testStateScmp.SUCCESS_NOT_EQUAL);
let service = TypeMoq.Mock.ofInstance(new SchemaCompareTestService());
service.setup(x => x.schemaCompareGetDefaultOptions()).returns(x => sc.schemaCompareGetDefaultOptions());
service.setup(x => x.schemaCompare(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => sc.schemaCompare('', undefined, undefined, undefined, undefined));
return service;
}
});
let showErrorMessageStub: any;
describe('SchemaCompareMainWindow.execute', function (): void {
before(() => {
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
@@ -93,16 +377,22 @@ describe('SchemaCompareMainWindow.execute', function (): void {
testContext = createContext();
});
beforeEach(function (): void {
testContext.apiWrapper.reset();
this.afterEach(() => {
sinon.restore();
});
this.beforeEach(() => {
sinon.restore();
});
it('Should fail for failing Schema Compare service', async function (): Promise<void> {
let sc = new SchemaCompareTestService(testStateScmp.FAILURE);
testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); });
let result = new SchemaCompareMainWindowTest(testContext.apiWrapper.object, sc, mockExtensionContext.object);
showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage').callsFake((message) => {
throw new Error(message);
});
let result = new SchemaCompareMainWindowTest(sc, mockExtensionContext.object);
await result.start(undefined);
should(result.getComparisonResult() === undefined);
@@ -116,8 +406,7 @@ describe('SchemaCompareMainWindow.execute', function (): void {
it('Should exit for failing Schema Compare service', async function (): Promise<void> {
let sc = new SchemaCompareTestService(testStateScmp.FAILURE);
testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns(() => Promise.resolve(''));
let result = new SchemaCompareMainWindowTest(testContext.apiWrapper.object, sc, mockExtensionContext.object);
let result = new SchemaCompareMainWindowTest(sc, mockExtensionContext.object);
await result.start(undefined);
@@ -127,13 +416,12 @@ describe('SchemaCompareMainWindow.execute', function (): void {
result.targetEndpointInfo = setDacpacEndpointInfo(mocktarget);
await result.execute();
testContext.apiWrapper.verify(x => x.showErrorMessage(TypeMoq.It.isAny()), TypeMoq.Times.once());
});
it('Should disable script button and apply button for Schema Compare service for dacpac', async function (): Promise<void> {
let sc = new SchemaCompareTestService(testStateScmp.SUCCESS_NOT_EQUAL);
let result = new SchemaCompareMainWindowTest(testContext.apiWrapper.object, sc, mockExtensionContext.object);
let result = new SchemaCompareMainWindowTest(sc, mockExtensionContext.object);
await result.start(undefined);
@@ -145,7 +433,7 @@ describe('SchemaCompareMainWindow.execute', function (): void {
await result.execute();
//Generate script button and apply button should be disabled for dacpac comparison
result.verifyButtonsState( {
result.verifyButtonsState({
compareButtonState: true,
optionsButtonState: true,
switchButtonState: true,
@@ -156,13 +444,13 @@ describe('SchemaCompareMainWindow.execute', function (): void {
selectTargetButtonState: true,
generateScriptButtonState: false,
applyButtonState: false
} );
});
});
it('Should disable script button and apply button for Schema Compare service for database', async function (): Promise<void> {
let sc = new SchemaCompareTestService(testStateScmp.SUCCESS_NOT_EQUAL);
let result = new SchemaCompareMainWindowTest(testContext.apiWrapper.object, sc, mockExtensionContext.object);
let result = new SchemaCompareMainWindowTest(sc, mockExtensionContext.object);
await result.start(undefined);
@@ -174,7 +462,7 @@ describe('SchemaCompareMainWindow.execute', function (): void {
await result.execute();
//Generate script button and apply button should be enabled for database comparison
result.verifyButtonsState( {
result.verifyButtonsState({
compareButtonState: true,
optionsButtonState: true,
switchButtonState: true,
@@ -185,7 +473,7 @@ describe('SchemaCompareMainWindow.execute', function (): void {
selectTargetButtonState: true,
generateScriptButtonState: true,
applyButtonState: true
} );
});
});
});
@@ -201,18 +489,18 @@ describe('SchemaCompareMainWindow.updateSourceAndTarget', function (): void {
let sc = new SchemaCompareTestService();
let endpointInfo: mssql.SchemaCompareEndpointInfo;
let result = new SchemaCompareMainWindowTest(testContext.apiWrapper.object, sc, mockExtensionContext.object);
let result = new SchemaCompareMainWindowTest(sc, mockExtensionContext.object);
await result.start(undefined);
should(result.getComparisonResult() === undefined);
result.sourceEndpointInfo = {...endpointInfo};
result.targetEndpointInfo = {...endpointInfo};
result.sourceEndpointInfo = { ...endpointInfo };
result.targetEndpointInfo = { ...endpointInfo };
result.updateSourceAndTarget();
result.verifyButtonsState( {
result.verifyButtonsState({
compareButtonState: false,
optionsButtonState: false,
switchButtonState: false,
@@ -223,25 +511,25 @@ describe('SchemaCompareMainWindow.updateSourceAndTarget', function (): void {
selectTargetButtonState: true,
generateScriptButtonState: false,
applyButtonState: false
} );
});
});
it('Should set buttons appropriately when source endpoint is empty and target endpoint is populated', async function (): Promise<void> {
let sc = new SchemaCompareTestService();
let endpointInfo: mssql.SchemaCompareEndpointInfo;
let result = new SchemaCompareMainWindowTest(testContext.apiWrapper.object, sc, mockExtensionContext.object);
let result = new SchemaCompareMainWindowTest(sc, mockExtensionContext.object);
await result.start(undefined);
should(result.getComparisonResult() === undefined);
result.sourceEndpointInfo = {...endpointInfo};
result.sourceEndpointInfo = { ...endpointInfo };
result.targetEndpointInfo = setDacpacEndpointInfo(mocktarget);
result.updateSourceAndTarget();
result.verifyButtonsState( {
result.verifyButtonsState({
compareButtonState: false,
optionsButtonState: false,
switchButtonState: true,
@@ -252,14 +540,14 @@ describe('SchemaCompareMainWindow.updateSourceAndTarget', function (): void {
selectTargetButtonState: true,
generateScriptButtonState: false,
applyButtonState: false
} );
});
});
it('Should set buttons appropriately when source and target endpoints are populated', async function (): Promise<void> {
let sc = new SchemaCompareTestService();
let endpointInfo: mssql.SchemaCompareEndpointInfo;
let result = new SchemaCompareMainWindowTest(testContext.apiWrapper.object, sc, mockExtensionContext.object);
let result = new SchemaCompareMainWindowTest(sc, mockExtensionContext.object);
await result.start(undefined);
@@ -270,7 +558,7 @@ describe('SchemaCompareMainWindow.updateSourceAndTarget', function (): void {
result.updateSourceAndTarget();
result.verifyButtonsState( {
result.verifyButtonsState({
compareButtonState: true,
optionsButtonState: true,
switchButtonState: true,
@@ -281,7 +569,7 @@ describe('SchemaCompareMainWindow.updateSourceAndTarget', function (): void {
selectTargetButtonState: true,
generateScriptButtonState: false,
applyButtonState: false
} );
});
});
});

View File

@@ -25,14 +25,14 @@ before(function (): void {
testContext = createContext();
});
describe('SchemaCompareDialog.openDialog', function (): void {
describe('SchemaCompareDialog.openDialog @DacFx@', function (): void {
before(() => {
mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
mockExtensionContext.setup(x => x.extensionPath).returns(() => '');
});
it('Should be correct when created.', async function (): Promise<void> {
let schemaCompareResult = new SchemaCompareMainWindow(testContext.apiWrapper.object, undefined, mockExtensionContext.object);
let schemaCompareResult = new SchemaCompareMainWindow(undefined, mockExtensionContext.object);
let dialog = new SchemaCompareDialog(schemaCompareResult);
await dialog.openDialog();
@@ -42,7 +42,7 @@ describe('SchemaCompareDialog.openDialog', function (): void {
});
it('Simulate ok button- with both endpoints set to dacpac', async function (): Promise<void> {
let schemaCompareResult = new SchemaCompareMainWindowTest(testContext.apiWrapper.object, undefined, mockExtensionContext.object);
let schemaCompareResult = new SchemaCompareMainWindowTest(undefined, mockExtensionContext.object);
await schemaCompareResult.start(undefined);
schemaCompareResult.sourceEndpointInfo = setDacpacEndpointInfo(mocksource);
schemaCompareResult.targetEndpointInfo = setDacpacEndpointInfo(mocktarget);
@@ -53,7 +53,7 @@ describe('SchemaCompareDialog.openDialog', function (): void {
await dialog.execute();
// Confirm that ok button got clicked
schemaCompareResult.verifyButtonsState( {
schemaCompareResult.verifyButtonsState({
compareButtonState: true,
optionsButtonState: true,
switchButtonState: true,
@@ -64,6 +64,6 @@ describe('SchemaCompareDialog.openDialog', function (): void {
selectTargetButtonState: true,
generateScriptButtonState: false,
applyButtonState: false
} );
});
});
});

View File

@@ -5,11 +5,8 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as TypeMoq from 'typemoq';
import { ApiWrapper } from '../common/apiWrapper';
export interface TestContext {
apiWrapper: TypeMoq.IMock<ApiWrapper>;
context: vscode.ExtensionContext;
}
@@ -17,7 +14,6 @@ export function createContext(): TestContext {
let extensionPath = path.join(__dirname, '..', '..');
return {
apiWrapper: TypeMoq.Mock.ofType(ApiWrapper),
context: {
subscriptions: [],
workspaceState: {

View File

@@ -7,7 +7,6 @@ import * as vscode from 'vscode';
import * as mssql from '../../../mssql';
import * as should from 'should';
import { SchemaCompareMainWindow } from '../schemaCompareMainWindow';
import { ApiWrapper } from '../common/apiWrapper';
export interface ButtonState {
compareButtonState: boolean;
@@ -24,10 +23,9 @@ export interface ButtonState {
export class SchemaCompareMainWindowTest extends SchemaCompareMainWindow {
constructor(
apiWrapper: ApiWrapper,
schemaCompareService: mssql.ISchemaCompareService,
extensionContext: vscode.ExtensionContext) {
super(apiWrapper, schemaCompareService, extensionContext);
super(schemaCompareService, extensionContext);
}
// only for test

View File

@@ -47,7 +47,7 @@ export class SchemaCompareTestService implements mssql.ISchemaCompareService {
throw new Error('Method not implemented.');
}
schemaCompare(operationId: string, sourceEndpointInfo: mssql.SchemaCompareEndpointInfo, targetEndpointInfo: mssql.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode): Thenable<mssql.SchemaCompareResult> {
schemaCompare(operationId: string, sourceEndpointInfo: mssql.SchemaCompareEndpointInfo, targetEndpointInfo: mssql.SchemaCompareEndpointInfo, taskExecutionMode: azdata.TaskExecutionMode, deploymentOptions: mssql.DeploymentOptions): Thenable<mssql.SchemaCompareResult> {
let result: mssql.SchemaCompareResult;
if (this.testState === testStateScmp.FAILURE) {
result = {

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