Compare commits

...

58 Commits

Author SHA1 Message Date
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
140 changed files with 3612 additions and 996 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,14 @@
# Change Log
## 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.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": [
"onCommand:arc.connectToController",
@@ -200,7 +200,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 +218,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 +244,7 @@
"fields": [
{
"type": "readonly_text",
"label": "%arc.data.controller.data.controller.connectivitymode.description%",
"label": "%arc.data.controller.connectivitymode.description%",
"labelWidth": "600px"
},
{
@@ -268,19 +268,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 +309,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 +355,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 +400,7 @@
]
},
{
"title": "%arc.data.controller.data.controller.create.summary.title%",
"title": "%arc.data.controller.create.summary.title%",
"isSummaryPage": true,
"fieldHeight": "16px",
"sections": [
@@ -528,18 +554,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 +578,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",

View File

@@ -20,18 +20,18 @@
"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.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 +39,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 +71,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

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

@@ -139,7 +139,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 +171,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");
@@ -205,3 +205,4 @@ export const noPasswordFound = (controllerName: string) => localize('noPasswordF
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

@@ -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,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';
@@ -39,7 +40,7 @@ describe('ControllerModel', function (): 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());
await should(model.azdataLogin()).be.rejectedWith(new UserCancelledError(loc.userCancelledError));
});
it('Reads password from cred store', async function (): Promise<void> {
@@ -64,7 +65,7 @@ describe('ControllerModel', function (): void {
});
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>();
@@ -90,7 +91,7 @@ describe('ControllerModel', function (): void {
});
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' }));
@@ -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' }));

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

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

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

@@ -321,6 +321,7 @@
},
"dependencies": {
"@azure/arm-resourcegraph": "^2.0.0",
"@azure/arm-storage": "^15.1.0",
"@azure/arm-subscriptions": "1.0.0",
"axios": "^0.19.2",
"qs": "^6.9.1",

View File

@@ -6,6 +6,7 @@
declare module 'azureResource' {
import { TreeDataProvider } from 'vscode';
import { DataProvider, Account, TreeItem } from 'azdata';
import { FileShareItem, ListContainerItem } from '@azure/arm-storage/esm/models';
export namespace azureResource {
export const enum AzureResourceType {
@@ -18,7 +19,8 @@ declare module 'azureResource' {
kustoClusters = 'microsoft.kusto/clusters',
azureArcPostgresServer = 'microsoft.azuredata/postgresinstances',
postgresServer = 'microsoft.dbforpostgresql/servers',
azureArcService = 'microsoft.azuredata/datacontrollers'
azureArcService = 'microsoft.azuredata/datacontrollers',
storageAccount = 'microsoft.storage/storageaccounts',
}
export interface IAzureResourceProvider extends DataProvider {
@@ -75,7 +77,10 @@ declare module 'azureResource' {
fullName: string;
defaultDatabaseName: string;
}
export interface BlobContainer extends ListContainerItem {
}
export interface FileShare extends FileShareItem {
}
}
}

View File

@@ -5,14 +5,18 @@
import { ResourceGraphClient } from '@azure/arm-resourcegraph';
import { TokenCredentials } from '@azure/ms-rest-js';
import axios, { AxiosRequestConfig } from 'axios';
import * as azdata from 'azdata';
import { GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult } from 'azurecore';
import { HttpGetRequestResult, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult } from 'azurecore';
import { azureResource } from 'azureResource';
import { EOL } from 'os';
import * as nls from 'vscode-nls';
import { AppContext } from '../appContext';
import { invalidAzureAccount, invalidTenant, unableToFetchTokenError } from '../localizedConstants';
import { AzureResourceServiceNames } from './constants';
import { IAzureResourceSubscriptionFilterService, IAzureResourceSubscriptionService } from './interfaces';
import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService';
import { StorageManagementClient } from '@azure/arm-storage';
const localize = nls.loadMessageBundle();
@@ -106,7 +110,7 @@ export function equals(one: any, other: any): boolean {
export async function getResourceGroups(appContext: AppContext, account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false): Promise<GetResourceGroupsResult> {
const result: GetResourceGroupsResult = { resourceGroups: [], errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants) || !subscription) {
const error = new Error(localize('azure.accounts.getResourceGroups.invalidParamsError', "Invalid account or subscription"));
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
@@ -146,7 +150,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
query: string): Promise<ResourceQueryResult<T>> {
const result: ResourceQueryResult<T> = { resources: [], errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
const error = new Error(localize('azure.accounts.runResourceQuery.errors.invalidAccount', "Invalid account"));
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
@@ -157,7 +161,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
// Check our subscriptions to ensure we have valid ones
subscriptions.forEach(subscription => {
if (!subscription.tenant) {
const error = new Error(localize('azure.accounts.runResourceQuery.errors.noTenantSpecifiedForSubscription', "Invalid tenant for subscription"));
const error = new Error(invalidTenant);
if (!ignoreErrors) {
throw error;
}
@@ -188,7 +192,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
resourceClient = new ResourceGraphClient(credential, { baseUri: account.properties.providerSettings.settings.armResource.endpoint });
} catch (err) {
console.error(err);
const error = new Error(localize('azure.accounts.runResourceQuery.errors.unableToFetchToken', "Unable to get token for tenant {0}", tenant.id));
const error = new Error(unableToFetchTokenError(tenant.id));
result.errors.push(error);
continue;
}
@@ -227,7 +231,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
export async function getSubscriptions(appContext: AppContext, account?: azdata.Account, ignoreErrors: boolean = false): Promise<GetSubscriptionsResult> {
const result: GetSubscriptionsResult = { subscriptions: [], errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
const error = new Error(localize('azure.accounts.getSubscriptions.invalidParamsError', "Invalid account"));
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
@@ -261,7 +265,7 @@ export async function getSubscriptions(appContext: AppContext, account?: azdata.
export async function getSelectedSubscriptions(appContext: AppContext, account?: azdata.Account, ignoreErrors: boolean = false): Promise<GetSubscriptionsResult> {
const result: GetSubscriptionsResult = { subscriptions: [], errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
const error = new Error(localize('azure.accounts.getSelectedSubscriptions.invalidParamsError', "Invalid account"));
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
@@ -284,3 +288,189 @@ export async function getSelectedSubscriptions(appContext: AppContext, account?:
}
return result;
}
/**
* makes a GET request to Azure REST apis. Currently, it only supports GET ARM queries.
*/
export async function makeHttpGetRequest(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false, url: string): Promise<HttpGetRequestResult> {
const result: HttpGetRequestResult = { response: {}, errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
if (!subscription.tenant) {
const error = new Error(invalidTenant);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
if (result.errors.length > 0) {
return result;
}
let securityToken: { token: string, tokenType?: string };
try {
securityToken = await azdata.accounts.getAccountSecurityToken(
account,
subscription.tenant!,
azdata.AzureResource.ResourceManagement
);
} catch (err) {
console.error(err);
const error = new Error(unableToFetchTokenError(subscription.tenant));
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
if (result.errors.length > 0) {
return result;
}
const config: AxiosRequestConfig = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${securityToken.token}`
},
validateStatus: () => true // Never throw
};
const response = await axios.get(url, config);
if (response.status !== 200) {
let errorMessage: string[] = [];
errorMessage.push(response.status.toString());
errorMessage.push(response.statusText);
if (response.data && response.data.error) {
errorMessage.push(`${response.data.error.code} : ${response.data.error.message}`);
}
const error = new Error(errorMessage.join(EOL));
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
result.response = response;
return result;
}
export async function getBlobContainers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccounts: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise<GetBlobContainersResult> {
let result: GetBlobContainersResult = { blobContainer: undefined, errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
if (!subscription.tenant) {
const error = new Error(invalidTenant);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
if (result.errors.length > 0) {
return result;
}
let securityToken: { token: string, tokenType?: string };
let credential: TokenCredentials;
try {
securityToken = await azdata.accounts.getAccountSecurityToken(
account,
subscription.tenant!,
azdata.AzureResource.ResourceManagement
);
const token = securityToken.token;
const tokenType = securityToken.tokenType;
credential = new TokenCredentials(token, tokenType);
} catch (err) {
console.error(err);
const error = new Error(unableToFetchTokenError(subscription.tenant));
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
try {
const client = new StorageManagementClient(<any>credential, subscription.id);
result.blobContainer = await client.blobContainers.list(storageAccounts.resourceGroup, storageAccounts.name);
} catch (err) {
console.error(err);
if (!ignoreErrors) {
throw err;
}
result.errors.push(err);
}
return result;
}
export async function getFileShares(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccounts: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise<GetFileSharesResult> {
let result: GetFileSharesResult = { fileShares: undefined, errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
if (!subscription.tenant) {
const error = new Error(invalidTenant);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
if (result.errors.length > 0) {
return result;
}
let securityToken: { token: string, tokenType?: string };
let credential: TokenCredentials;
try {
securityToken = await azdata.accounts.getAccountSecurityToken(
account,
subscription.tenant!,
azdata.AzureResource.ResourceManagement
);
const token = securityToken.token;
const tokenType = securityToken.tokenType;
credential = new TokenCredentials(token, tokenType);
} catch (err) {
console.error(err);
const error = new Error(unableToFetchTokenError(subscription.tenant));
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
try {
const client = new StorageManagementClient(<any>credential, subscription.id);
result.fileShares = await client.fileShares.list(storageAccounts.resourceGroup, storageAccounts.name);
} catch (err) {
console.error(err);
if (!ignoreErrors) {
throw err;
}
result.errors.push(err);
}
return result;
}

View File

@@ -6,6 +6,8 @@
declare module 'azurecore' {
import * as azdata from 'azdata';
import { azureResource } from 'azureResource';
import { BlobContainersListResponse, FileSharesListResponse } from '@azure/arm-storage/esm/models';
/**
* Covers defining what the azurecore extension exports to other extensions
*
@@ -66,8 +68,14 @@ declare module 'azurecore' {
}
export interface IExtension {
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly?: boolean): Thenable<GetSubscriptionsResult>;
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Thenable<GetResourceGroupsResult>;
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly?: boolean): Promise<GetSubscriptionsResult>;
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise<GetResourceGroupsResult>;
getSqlManagedInstances(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlManagedInstancesResult>;
getSqlServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlServersResult>;
getSqlVMServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlVMServersResult>;
getStorageAccounts(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetStorageAccountResult>;
getBlobContainers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise<GetBlobContainersResult>;
getFileShares(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise<GetFileSharesResult>;
/**
* Converts a region value (@see AzureRegion) into the localized Display Name
* @param region The region value
@@ -76,10 +84,18 @@ declare module 'azurecore' {
provideResources(): azureResource.IAzureResourceProvider[];
runGraphQuery<T extends azureResource.AzureGraphResource>(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors: boolean, query: string): Promise<ResourceQueryResult<T>>;
makeHttpGetRequest(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, ignoreErrors: boolean, url: string): Promise<HttpGetRequestResult>;
}
export type GetSubscriptionsResult = { subscriptions: azureResource.AzureResourceSubscription[], errors: Error[] };
export type GetResourceGroupsResult = { resourceGroups: azureResource.AzureResourceResourceGroup[], errors: Error[] };
export type GetSqlManagedInstancesResult = { resources: azureResource.AzureGraphResource[], errors: Error[] };
export type GetSqlServersResult = {resources: azureResource.AzureGraphResource[], errors: Error[]};
export type GetSqlVMServersResult = {resources: azureResource.AzureGraphResource[], errors: Error[]};
export type GetStorageAccountResult = {resources: azureResource.AzureGraphResource[], errors: Error[]};
export type GetBlobContainersResult = {blobContainer: BlobContainersListResponse | undefined, errors: Error[]};
export type GetFileSharesResult = {fileShares: FileSharesListResponse | undefined, errors: Error[]};
export type ResourceQueryResult<T extends azureResource.AzureGraphResource> = { resources: T[], errors: Error[] };
export type HttpGetRequestResult = { response: any, errors: Error[] };
}

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,13 +106,47 @@ 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> {
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly: boolean = false): Promise<azurecore.GetSubscriptionsResult> {
return selectedOnly
? azureResourceUtils.getSelectedSubscriptions(appContext, account, ignoreErrors)
: azureResourceUtils.getSubscriptions(appContext, account, ignoreErrors);
},
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Thenable<azurecore.GetResourceGroupsResult> { return azureResourceUtils.getResourceGroups(appContext, account, subscription, ignoreErrors); },
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise<azurecore.GetResourceGroupsResult> { return azureResourceUtils.getResourceGroups(appContext, account, subscription, ignoreErrors); },
provideResources(): azureResource.IAzureResourceProvider[] {
const arcFeaturedEnabled = vscode.workspace.getConfiguration(constants.extensionConfigSectionName).get('enableArcFeatures');
const providers: azureResource.IAzureResourceProvider[] = [
@@ -129,12 +164,50 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
}
return providers;
},
getSqlManagedInstances(account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],
ignoreErrors: boolean): Promise<azurecore.GetSqlManagedInstancesResult> {
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.sqlManagedInstance}"`);
},
getSqlServers(account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],
ignoreErrors: boolean): Promise<azurecore.GetSqlServersResult> {
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.sqlServer}"`);
},
getSqlVMServers(account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],
ignoreErrors: boolean): Promise<azurecore.GetSqlVMServersResult> {
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.virtualMachines}" and properties.storageProfile.imageReference.publisher == "microsoftsqlserver"`);
},
getStorageAccounts(account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],
ignoreErrors: boolean): Promise<azurecore.GetStorageAccountResult> {
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.storageAccount}"`);
},
getBlobContainers(account: azdata.Account,
subscription: azureResource.AzureResourceSubscription,
storageAccount: azureResource.AzureGraphResource,
ignoreErrors: boolean): Promise<azurecore.GetBlobContainersResult> {
return azureResourceUtils.getBlobContainers(account, subscription, storageAccount, ignoreErrors);
},
getFileShares(account: azdata.Account,
subscription: azureResource.AzureResourceSubscription,
storageAccount: azureResource.AzureGraphResource,
ignoreErrors: boolean): Promise<azurecore.GetFileSharesResult> {
return azureResourceUtils.getFileShares(account, subscription, storageAccount, ignoreErrors);
},
getRegionDisplayName: utils.getRegionDisplayName,
runGraphQuery<T extends azureResource.AzureGraphResource>(account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],
ignoreErrors: boolean,
query: string): Promise<azurecore.ResourceQueryResult<T>> {
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, query);
},
makeHttpGetRequest(account: azdata.Account,
subscription: azureResource.AzureResourceSubscription,
ignoreErrors: boolean,
url: string) {
return azureResourceUtils.makeHttpGetRequest(account, subscription, ignoreErrors, url);
}
};
}

View File

@@ -74,3 +74,10 @@ export const azureArcPostgresServer = localize('azurecore.azureArcPostgres', "Az
export const unableToOpenAzureLink = localize('azure.unableToOpenAzureLink', "Unable to open link, missing required values");
export const azureResourcesGridTitle = localize('azure.azureResourcesGridTitle', "Azure Resources (Preview)");
// Azure Request Errors
export const invalidAzureAccount = localize('azurecore.invalidAzureAccount', "Invalid account");
export const invalidTenant = localize('azurecore.invalidTenant', "Invalid tenant for subscription");
export function unableToFetchTokenError(tenant: string): string {
return localize('azurecore.unableToFetchToken', "Unable to get token for tenant {0}", tenant);
}

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

@@ -11,6 +11,15 @@
"@azure/ms-rest-js" "^1.8.1"
tslib "^1.9.3"
"@azure/arm-storage@^15.1.0":
version "15.1.0"
resolved "https://registry.yarnpkg.com/@azure/arm-storage/-/arm-storage-15.1.0.tgz#fa14b5e532babf39b47c5cffe89de5aa062e1f80"
integrity sha512-IWomHlT7eEnCSMDHH/z5/XyPHhGAIPmWYgHkIyYB2YQt+Af+hWvE1NIwI79Eeiu+Am4U8BKUsXWmWKqDYh0Srg==
dependencies:
"@azure/ms-rest-azure-js" "^2.0.1"
"@azure/ms-rest-js" "^2.0.4"
tslib "^1.10.0"
"@azure/arm-subscriptions@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@azure/arm-subscriptions/-/arm-subscriptions-1.0.0.tgz#ab65a5cd4d8b8c878ff6621428f29137b84eb1d6"
@@ -28,6 +37,14 @@
"@azure/ms-rest-js" "^1.8.10"
tslib "^1.9.3"
"@azure/ms-rest-azure-js@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@azure/ms-rest-azure-js/-/ms-rest-azure-js-2.0.1.tgz#fa1b38f039b3ee48a9e086a88c8a5b5b7776491c"
integrity sha512-5e+A710O7gRFISoV4KI/ZyLQbKmjXxQZ1L8Z/sx7jSUQqmswjTnN4yyIZxs5JzfLVkobU0rXxbi5/LVzaI8QXQ==
dependencies:
"@azure/ms-rest-js" "^2.0.4"
tslib "^1.10.0"
"@azure/ms-rest-js@^1.1.0", "@azure/ms-rest-js@^1.8.1", "@azure/ms-rest-js@^1.8.10":
version "1.8.14"
resolved "https://registry.yarnpkg.com/@azure/ms-rest-js/-/ms-rest-js-1.8.14.tgz#657fc145db20b6eb3d58d1a2055473aa72eb609d"
@@ -42,6 +59,22 @@
uuid "^3.2.1"
xml2js "^0.4.19"
"@azure/ms-rest-js@^2.0.4":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@azure/ms-rest-js/-/ms-rest-js-2.1.0.tgz#41bc541984983b5242dfbcf699ea281acd045946"
integrity sha512-4BXLVImYRt+jcUmEJ5LUWglI8RBNVQndY6IcyvQ4U8O4kIXdmlRz3cJdA/RpXf5rKT38KOoTO2T6Z1f6Z1HDBg==
dependencies:
"@types/node-fetch" "^2.3.7"
"@types/tunnel" "0.0.1"
abort-controller "^3.0.0"
form-data "^2.5.0"
node-fetch "^2.6.0"
tough-cookie "^3.0.1"
tslib "^1.10.0"
tunnel "0.0.6"
uuid "^3.3.2"
xml2js "^0.4.19"
"@babel/code-frame@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
@@ -275,6 +308,14 @@
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea"
integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==
"@types/node-fetch@^2.3.7":
version "2.5.7"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c"
integrity sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==
dependencies:
"@types/node" "*"
form-data "^3.0.0"
"@types/node@*":
version "13.9.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.5.tgz#59738bf30b31aea1faa2df7f4a5f55613750cf00"
@@ -324,6 +365,13 @@
dependencies:
"@types/node" "*"
"@types/tunnel@0.0.1":
version "0.0.1"
resolved "https://registry.yarnpkg.com/@types/tunnel/-/tunnel-0.0.1.tgz#0d72774768b73df26f25df9184273a42da72b19c"
integrity sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A==
dependencies:
"@types/node" "*"
"@types/ws@^6.0.4":
version "6.0.4"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.4.tgz#7797707c8acce8f76d8c34b370d4645b70421ff1"
@@ -331,6 +379,13 @@
dependencies:
"@types/node" "*"
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
dependencies:
event-target-shim "^5.0.0"
ansi-regex@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
@@ -466,7 +521,7 @@ color-name@1.1.3:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
combined-stream@^1.0.6:
combined-stream@^1.0.6, combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
@@ -596,6 +651,11 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
event-target-shim@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
expand-template@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
@@ -617,6 +677,15 @@ form-data@^2.3.2, form-data@^2.5.0:
combined-stream "^1.0.6"
mime-types "^2.1.12"
form-data@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682"
integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
fs-constants@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
@@ -733,6 +802,11 @@ ini@~1.3.0:
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
ip-regex@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=
is-buffer@~1.1.1:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@@ -1009,6 +1083,11 @@ node-abi@^2.7.0:
dependencies:
semver "^5.4.1"
node-fetch@^2.6.0:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
noop-logger@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2"
@@ -1389,6 +1468,20 @@ tough-cookie@^2.4.3:
psl "^1.1.28"
punycode "^2.1.1"
tough-cookie@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
dependencies:
ip-regex "^2.1.0"
psl "^1.1.28"
punycode "^2.1.1"
tslib@^1.10.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^1.9.2, tslib@^1.9.3:
version "1.11.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
@@ -1425,7 +1518,7 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
uuid@^3.2.1:
uuid@^3.2.1, uuid@^3.3.2:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==

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

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

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

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

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

@@ -8,13 +8,34 @@ import * as azurecore from 'azurecore';
import { azureResource } from 'azureResource';
export class AzurecoreApiStub implements azurecore.IExtension {
getFileShares(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _storageAccount: azureResource.AzureGraphResource, _ignoreErrors?: boolean): Promise<azurecore.GetFileSharesResult> {
throw new Error('Method not implemented.');
}
getBlobContainers(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _storageAccount: azureResource.AzureGraphResource, _ignoreErrors?: boolean): Promise<azurecore.GetBlobContainersResult> {
throw new Error('Method not implemented.');
}
getSqlManagedInstances(_account: azdata.Account, _subscriptions: azureResource.AzureResourceSubscription[], _ignoreErrors?: boolean): Promise<azurecore.GetSqlManagedInstancesResult> {
throw new Error('Method not implemented.');
}
getSqlServers(_account: azdata.Account, _subscriptions: azureResource.AzureResourceSubscription[], _ignoreErrors?: boolean): Promise<azurecore.GetSqlServersResult> {
throw new Error('Method not implemented.');
}
getSqlVMServers(_account: azdata.Account, _subscriptions: azureResource.AzureResourceSubscription[], _ignoreErrors?: boolean): Promise<azurecore.GetSqlVMServersResult> {
throw new Error('Method not implemented.');
}
getStorageAccounts(_account: azdata.Account, _subscriptions: azureResource.AzureResourceSubscription[], _ignoreErrors?: boolean): Promise<azurecore.GetStorageAccountResult> {
throw new Error('Method not implemented.');
}
makeHttpGetRequest(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _ignoreErrors: boolean, _url: string): Promise<any> {
throw new Error('Method not implemented.');
}
runGraphQuery<T extends azureResource.AzureGraphResource>(_account: azdata.Account, _subscriptions: azureResource.AzureResourceSubscription[], _ignoreErrors: boolean, _query: string): Promise<azurecore.ResourceQueryResult<T>> {
throw new Error('Method not implemented.');
}
getSubscriptions(_account?: azdata.Account | undefined, _ignoreErrors?: boolean | undefined, _selectedOnly?: boolean | undefined): Thenable<azurecore.GetSubscriptionsResult> {
getSubscriptions(_account?: azdata.Account | undefined, _ignoreErrors?: boolean | undefined, _selectedOnly?: boolean | undefined): Promise<azurecore.GetSubscriptionsResult> {
throw new Error('Method not implemented.');
}
getResourceGroups(_account?: azdata.Account | undefined, _subscription?: azureResource.AzureResourceSubscription | undefined, _ignoreErrors?: boolean | undefined): Thenable<azurecore.GetResourceGroupsResult> {
getResourceGroups(_account?: azdata.Account | undefined, _subscription?: azureResource.AzureResourceSubscription | undefined, _ignoreErrors?: boolean | undefined): Promise<azurecore.GetResourceGroupsResult> {
throw new Error('Method not implemented.');
}
getRegionDisplayName(_region?: string | undefined): string {

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.61",
"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

@@ -261,6 +261,11 @@ export interface DynamicEnablementInfo {
value: string
}
export interface ValueProviderInfo {
providerId: string,
triggerField: string
}
export interface FieldInfoBase {
labelWidth?: string;
inputWidth?: string;
@@ -307,9 +312,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();
@@ -623,6 +665,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 +702,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 +799,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;
@@ -938,6 +983,7 @@ async function createRadioOptions(context: FieldContext, getRadioButtonInfo?: ((
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,
});
@@ -1133,6 +1179,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 +1441,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 = {

View File

@@ -5,6 +5,7 @@
import * as should from 'should';
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as mssql from '../../../mssql';
import * as loc from '../localizedConstants';
import * as TypeMoq from 'typemoq';
@@ -15,10 +16,15 @@ import { promises as fs } from 'fs';
import { getEndpointName, verifyConnectionAndGetOwnerUri, exists } from '../utils';
import { mockDacpacEndpoint, mockDatabaseEndpoint, mockFilePath, mockConnectionInfo, shouldThrowSpecificError, mockConnectionResult, mockConnectionProfile } from './testUtils';
import { createContext, TestContext } from './testContext';
import * as sinon from 'sinon';
let testContext: TestContext;
describe('utils: Tests to verify getEndpointName', function (): void {
describe('utils: Tests to verify getEndpointName @DacFx@', function (): void {
afterEach(() => {
sinon.restore();
});
it('Should generate correct endpoint information', () => {
let endpointInfo: mssql.SchemaCompareEndpointInfo;
@@ -52,7 +58,7 @@ describe('utils: Basic tests to verify verifyConnectionAndGetOwnerUri', function
it('Should return undefined for endpoint as dacpac', async function (): Promise<void> {
let ownerUri = undefined;
ownerUri = await verifyConnectionAndGetOwnerUri(mockDacpacEndpoint, 'test', testContext.apiWrapper.object);
ownerUri = await verifyConnectionAndGetOwnerUri(mockDacpacEndpoint, 'test');
should(ownerUri).equal(undefined);
});
@@ -62,7 +68,7 @@ describe('utils: Basic tests to verify verifyConnectionAndGetOwnerUri', function
let testDatabaseEndpoint: mssql.SchemaCompareEndpointInfo = { ...mockDatabaseEndpoint };
testDatabaseEndpoint.connectionDetails = undefined;
ownerUri = await verifyConnectionAndGetOwnerUri(testDatabaseEndpoint, 'test', testContext.apiWrapper.object);
ownerUri = await verifyConnectionAndGetOwnerUri(testDatabaseEndpoint, 'test');
should(ownerUri).equal(undefined);
});
@@ -73,6 +79,10 @@ describe('utils: In-depth tests to verify verifyConnectionAndGetOwnerUri', funct
testContext = createContext();
});
afterEach(() => {
sinon.restore();
});
it('Should throw an error asking to make a connection', async function (): Promise<void> {
let getConnectionsResults: azdata.connection.ConnectionProfile[] = [];
let connection = { ...mockConnectionResult };
@@ -80,12 +90,14 @@ describe('utils: In-depth tests to verify verifyConnectionAndGetOwnerUri', funct
testDatabaseEndpoint.connectionDetails = { ...mockConnectionInfo };
const getConnectionString = loc.getConnectionString('test');
testContext.apiWrapper.setup(x => x.connect(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { return Promise.resolve(connection); });
testContext.apiWrapper.setup(x => x.getUriForConnection(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(undefined); });
testContext.apiWrapper.setup(x => x.getConnections(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(getConnectionsResults); });
testContext.apiWrapper.setup(x => x.showWarningMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((s) => { throw new Error(s); });
sinon.stub(azdata.connection, 'connect').returns(<any>Promise.resolve(connection));
sinon.stub(azdata.connection, 'getUriForConnection').returns(<any>Promise.resolve(undefined));
sinon.stub(azdata.connection, 'getConnections').returns(<any>Promise.resolve(getConnectionsResults));
sinon.stub(vscode.window, 'showWarningMessage').callsFake((message) => {
throw new Error(message);
});
await shouldThrowSpecificError(async () => await verifyConnectionAndGetOwnerUri(testDatabaseEndpoint, 'test', testContext.apiWrapper.object), getConnectionString);
await shouldThrowSpecificError(async () => await verifyConnectionAndGetOwnerUri(testDatabaseEndpoint, 'test'), getConnectionString);
});
it('Should throw an error for login failure', async function (): Promise<void> {
@@ -94,12 +106,15 @@ describe('utils: In-depth tests to verify verifyConnectionAndGetOwnerUri', funct
let testDatabaseEndpoint: mssql.SchemaCompareEndpointInfo = { ...mockDatabaseEndpoint };
testDatabaseEndpoint.connectionDetails = { ...mockConnectionInfo };
testContext.apiWrapper.setup(x => x.connect(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { return Promise.resolve(connection); });
testContext.apiWrapper.setup(x => x.getUriForConnection(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(undefined); });
testContext.apiWrapper.setup(x => x.getConnections(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(getConnectionsResults); });
testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); });
sinon.stub(azdata.connection, 'connect').returns(<any>Promise.resolve(connection));
sinon.stub(azdata.connection, 'getUriForConnection').returns(<any>Promise.resolve(undefined));
sinon.stub(azdata.connection, 'getConnections').returns(<any>Promise.resolve(getConnectionsResults));
sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve(loc.YesButtonText));
sinon.stub(vscode.window, 'showErrorMessage').callsFake((message) => {
throw new Error(message);
});
await shouldThrowSpecificError(async () => await verifyConnectionAndGetOwnerUri(testDatabaseEndpoint, 'test', testContext.apiWrapper.object), connection.errorMessage);
await shouldThrowSpecificError(async () => await verifyConnectionAndGetOwnerUri(testDatabaseEndpoint, 'test'), connection.errorMessage);
});
it('Should throw an error for login failure with openConnectionDialog but no ownerUri', async function (): Promise<void> {
@@ -108,13 +123,18 @@ describe('utils: In-depth tests to verify verifyConnectionAndGetOwnerUri', funct
let testDatabaseEndpoint: mssql.SchemaCompareEndpointInfo = { ...mockDatabaseEndpoint };
testDatabaseEndpoint.connectionDetails = { ...mockConnectionInfo };
testContext.apiWrapper.setup(x => x.connect(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { return Promise.resolve(connection); });
testContext.apiWrapper.setup(x => x.getUriForConnection(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(undefined); });
testContext.apiWrapper.setup(x => x.getConnections(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(getConnectionsResults); });
testContext.apiWrapper.setup(x => x.showWarningMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { return Promise.resolve(loc.YesButtonText); });
testContext.apiWrapper.setup(x => x.openConnectionDialog(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { return Promise.resolve(undefined); });
sinon.stub(azdata.connection, 'connect').returns(<any>Promise.resolve(connection));
sinon.stub(azdata.connection, 'getUriForConnection').returns(<any>Promise.resolve(undefined));
sinon.stub(azdata.connection, 'openConnectionDialog').returns(<any>Promise.resolve({
connectionId: 'id'
}));
sinon.stub(azdata.connection, 'getConnections').returns(<any>Promise.resolve(getConnectionsResults));
sinon.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve(loc.YesButtonText));
sinon.stub(vscode.window, 'showErrorMessage').callsFake((message) => {
throw new Error(message);
});
await shouldThrowSpecificError(async () => await verifyConnectionAndGetOwnerUri(testDatabaseEndpoint, 'test', testContext.apiWrapper.object), connection.errorMessage);
await shouldThrowSpecificError(async () => await verifyConnectionAndGetOwnerUri(testDatabaseEndpoint, 'test'), connection.errorMessage);
});
it('Should not throw an error and set ownerUri appropriately', async function (): Promise<void> {
@@ -124,10 +144,10 @@ describe('utils: In-depth tests to verify verifyConnectionAndGetOwnerUri', funct
let expectedOwnerUri: string = 'providerName:MSSQL|authenticationType:SqlLogin|database:My Database|server:My Server|user:My User|databaseDisplayName:My Database';
testDatabaseEndpoint.connectionDetails = { ...mockConnectionInfo };
testContext.apiWrapper.setup(x => x.connect(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { return Promise.resolve(connection); });
testContext.apiWrapper.setup(x => x.getUriForConnection(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(expectedOwnerUri); });
sinon.stub(azdata.connection, 'connect').returns(<any>Promise.resolve(connection));
sinon.stub(azdata.connection, 'getUriForConnection').returns(<any>Promise.resolve(expectedOwnerUri));
ownerUri = await verifyConnectionAndGetOwnerUri(testDatabaseEndpoint, 'test', testContext.apiWrapper.object);
ownerUri = await verifyConnectionAndGetOwnerUri(testDatabaseEndpoint, 'test');
should(ownerUri).equal(expectedOwnerUri);
});

View File

@@ -8,7 +8,6 @@ import * as vscode from 'vscode';
import * as mssql from '../../mssql';
import * as os from 'os';
import * as loc from './localizedConstants';
import { ApiWrapper } from './common/apiWrapper';
import { promises as fs } from 'fs';
export interface IPackageInfo {
@@ -32,7 +31,7 @@ export function getPackageInfo(packageJson: any): IPackageInfo {
* @param msg The error message to map
*/
export function getTelemetryErrorType(msg: string): string {
if (msg.indexOf('Object reference not set to an instance of an object') !== -1) {
if (msg && msg.indexOf('Object reference not set to an instance of an object') !== -1) {
return 'ObjectReferenceNotSet';
}
else {
@@ -85,18 +84,18 @@ function connectionInfoToConnectionProfile(details: azdata.ConnectionInfo): azda
};
}
export async function verifyConnectionAndGetOwnerUri(endpoint: mssql.SchemaCompareEndpointInfo, caller: string, apiWrapper: ApiWrapper): Promise<string | undefined> {
export async function verifyConnectionAndGetOwnerUri(endpoint: mssql.SchemaCompareEndpointInfo, caller: string): Promise<string | undefined> {
let ownerUri = undefined;
if (endpoint.endpointType === mssql.SchemaCompareEndpointType.Database && endpoint.connectionDetails) {
let connectionProfile = await connectionInfoToConnectionProfile(endpoint.connectionDetails);
let connection = await apiWrapper.connect(connectionProfile, false, false);
let connection = await azdata.connection.connect(connectionProfile, false, false);
if (connection) {
ownerUri = await apiWrapper.getUriForConnection(connection.connectionId);
ownerUri = await azdata.connection.getUriForConnection(connection.connectionId);
if (!ownerUri) {
let connectionList = await apiWrapper.getConnections(true);
let connectionList = await azdata.connection.getConnections(true);
let userConnection;
userConnection = connectionList.find(connection =>
@@ -109,18 +108,18 @@ export async function verifyConnectionAndGetOwnerUri(endpoint: mssql.SchemaCompa
if (userConnection === undefined) {
const getConnectionString = loc.getConnectionString(caller);
// need only yes button - since the modal dialog has a default cancel
let result = await apiWrapper.showWarningMessage(getConnectionString, { modal: true }, loc.YesButtonText);
let result = await vscode.window.showWarningMessage(getConnectionString, { modal: true }, loc.YesButtonText);
if (result === loc.YesButtonText) {
userConnection = await apiWrapper.openConnectionDialog(undefined, connectionProfile);
userConnection = await azdata.connection.openConnectionDialog(undefined, connectionProfile);
}
}
if (userConnection !== undefined) {
ownerUri = await apiWrapper.getUriForConnection(userConnection.connectionId);
ownerUri = await azdata.connection.getUriForConnection(userConnection.connectionId);
}
}
if (!ownerUri && connection.errorMessage) {
apiWrapper.showErrorMessage(connection.errorMessage);
vscode.window.showErrorMessage(connection.errorMessage);
}
}
}

View File

@@ -182,6 +182,42 @@
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd"
integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==
"@sinonjs/commons@^1", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.1":
version "1.8.1"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217"
integrity sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==
dependencies:
type-detect "4.0.8"
"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40"
integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==
dependencies:
"@sinonjs/commons" "^1.7.0"
"@sinonjs/formatio@^5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-5.0.1.tgz#f13e713cb3313b1ab965901b01b0828ea6b77089"
integrity sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==
dependencies:
"@sinonjs/commons" "^1"
"@sinonjs/samsam" "^5.0.2"
"@sinonjs/samsam@^5.0.2", "@sinonjs/samsam@^5.2.0":
version "5.3.0"
resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.3.0.tgz#1d2f0743dc54bf13fe9d508baefacdffa25d4329"
integrity sha512-hXpcfx3aq+ETVBwPlRFICld5EnrkexXuXDwqUNhDdr5L8VjvMeSRwyOa0qL7XFmR+jVWR4rUZtnxlG7RX72sBg==
dependencies:
"@sinonjs/commons" "^1.6.0"
lodash.get "^4.4.2"
type-detect "^4.0.8"
"@sinonjs/text-encoding@^0.7.1":
version "0.7.1"
resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5"
integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==
"@types/mocha@^5.2.5":
version "5.2.6"
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.6.tgz#b8622d50557dd155e9f2f634b7d68fd38de5e94b"
@@ -192,6 +228,18 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.7.tgz#01e4ea724d9e3bd50d90c11fd5980ba317d8fa11"
integrity sha512-E6Zn0rffhgd130zbCbAr/JdXfXkoOUFAKNs/rF8qnafSJ8KYaA/j3oz7dcwal+lYjLA7xvdd5J4wdYpCTlP8+w==
"@types/sinon@^9.0.4":
version "9.0.9"
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.9.tgz#115843b491583f924080f684b6d0d7438344f73c"
integrity sha512-z/y8maYOQyYLyqaOB+dYQ6i0pxKLOsfwCmHmn4T7jS/SDHicIslr37oE3Dg8SCqKrKeBy6Lemu7do2yy+unLrw==
dependencies:
"@types/sinonjs__fake-timers" "*"
"@types/sinonjs__fake-timers@*":
version "6.0.2"
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz#3a84cf5ec3249439015e14049bd3161419bf9eae"
integrity sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg==
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"
@@ -395,6 +443,11 @@ diff@3.5.0:
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
diff@^4.0.2:
version "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"
@@ -501,6 +554,11 @@ is-buffer@~1.1.1:
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
istanbul-lib-coverage@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49"
@@ -576,6 +634,16 @@ json5@^2.1.2:
dependencies:
minimist "^1.2.5"
just-extend@^4.0.2:
version "4.1.1"
resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.1.tgz#158f1fdb01f128c411dc8b286a7b4837b3545282"
integrity sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==
lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
lodash@^4.16.4, lodash@^4.17.13, lodash@^4.17.4:
version "4.17.19"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
@@ -675,6 +743,17 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
nise@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/nise/-/nise-4.0.4.tgz#d73dea3e5731e6561992b8f570be9e363c4512dd"
integrity sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==
dependencies:
"@sinonjs/commons" "^1.7.0"
"@sinonjs/fake-timers" "^6.0.0"
"@sinonjs/text-encoding" "^0.7.1"
just-extend "^4.0.2"
path-to-regexp "^1.7.0"
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -692,6 +771,13 @@ path-parse@^1.0.6:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
dependencies:
isarray "0.0.1"
pify@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
@@ -785,6 +871,19 @@ should@^13.2.1:
should-type-adaptors "^1.0.1"
should-util "^1.0.0"
sinon@^9.0.2:
version "9.2.1"
resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.2.1.tgz#64cc88beac718557055bd8caa526b34a2231be6d"
integrity sha512-naPfsamB5KEE1aiioaoqJ6MEhdUs/2vtI5w1hPAXX/UwvoPjXcwh1m5HiKx0HGgKR8lQSoFIgY5jM6KK8VrS9w==
dependencies:
"@sinonjs/commons" "^1.8.1"
"@sinonjs/fake-timers" "^6.0.1"
"@sinonjs/formatio" "^5.0.1"
"@sinonjs/samsam" "^5.2.0"
diff "^4.0.2"
nise "^4.0.4"
supports-color "^7.1.0"
source-map@^0.5.0:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@@ -838,6 +937,11 @@ to-fast-properties@^2.0.0:
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
type-detect@4.0.8, type-detect@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
typemoq@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/typemoq/-/typemoq-2.1.0.tgz#4452ce360d92cf2a1a180f0c29de2803f87af1e8"

View File

@@ -0,0 +1,47 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.4" d="M57.1348 30.379C62.6278 31.873 64.0168 32.817 68.9678 36.845C68.9308 36.781 68.8898 36.725 68.8478 36.656C68.6178 36.287 68.3678 35.936 68.1168 35.594C68.0228 35.474 67.9308 35.341 67.8318 35.221C67.6088 34.946 67.3688 34.694 67.1318 34.441C66.7601 34.0511 66.3654 33.6837 65.9498 33.341L65.9378 33.331C64.6334 32.2806 63.1432 31.4848 61.5448 30.985L61.3448 30.919C61.0068 30.819 60.6628 30.7323 60.3128 30.659C60.0398 30.604 59.7608 30.559 59.4808 30.521C59.2658 30.49 59.0528 30.448 58.8358 30.427C58.3244 30.3817 57.8121 30.362 57.2988 30.368L57.1348 30.379Z" fill="white"/>
<path d="M63.7865 24.777C60.1445 24.777 57.1865 23.708 57.1865 22.388V35.1C57.1865 36.408 60.0865 37.471 63.6925 37.488H63.7865C67.4295 37.488 70.3865 36.42 70.3865 35.1V22.388C70.3775 23.708 67.4245 24.777 63.7865 24.777Z" fill="url(#paint0_linear)"/>
<path d="M70.3775 22.388C70.3775 23.708 67.4245 24.777 63.7775 24.777C60.1305 24.777 57.1865 23.708 57.1865 22.388C57.1865 21.068 60.1405 20 63.7865 20C67.4325 20 70.3865 21.069 70.3865 22.388" fill="#E8E8E8"/>
<path d="M68.8376 22.2C68.8376 23.04 66.5746 23.719 63.7826 23.719C60.9906 23.719 58.7266 23.039 58.7266 22.2C58.7266 21.361 60.9896 20.682 63.7866 20.682C66.5836 20.682 68.8426 21.361 68.8426 22.2" fill="#50E6FF"/>
<path d="M63.7861 22.545C62.4301 22.5141 61.0784 22.7088 59.7861 23.121C62.393 23.9195 65.1792 23.9195 67.7861 23.121C66.4939 22.7088 65.1422 22.5141 63.7861 22.545Z" fill="#198AB3"/>
<path d="M79.6268 53.279C79.5407 50.42 78.441 47.6848 76.5241 45.5618C74.6073 43.4389 71.9982 42.0666 69.1628 41.69C69.0828 33.65 62.1088 27.158 53.5118 27.158C50.2817 27.0992 47.113 28.0452 44.4439 29.8652C41.7747 31.6853 39.7368 34.2895 38.6118 37.318C31.4408 38.41 25.9668 44.19 25.9668 51.16C25.9668 58.91 32.7308 65.192 41.0748 65.192C41.5238 65.192 41.9678 65.169 42.4068 65.134H66.8738C67.0926 65.1298 67.3099 65.0962 67.5198 65.034C74.2568 64.761 79.6268 59.6 79.6268 53.279Z" fill="url(#paint1_linear)"/>
<path d="M78.4631 29.993C76.8101 27.833 73.9731 26.783 70.3771 26.739V30.339C72.8971 30.381 74.7321 31.019 75.6161 32.178C78.6511 36.156 73.1371 49.2 57.2631 61.4C41.3891 73.6 27.4111 75.561 24.3781 71.581C22.6591 69.327 23.7251 64.151 27.8231 57.881C27.0849 56.6401 26.5543 55.2869 26.2521 53.875C20.3921 61.915 18.2761 69.501 21.5311 73.775C26.7481 80.616 43.7221 76.362 59.4441 64.275C75.1661 52.188 83.6791 36.833 78.4631 29.993Z" fill="#0072C6"/>
<path d="M48.1607 58.2C41.9137 58.2 36.8467 56.365 36.8467 54.1V75.9C36.8467 78.144 41.8297 79.967 48.0067 80H48.1617C54.4107 80 59.4757 78.166 59.4757 75.9V54.1C59.4747 56.368 54.4087 58.2 48.1607 58.2Z" fill="url(#paint2_linear)"/>
<path d="M59.4747 54.105C59.4747 56.368 54.4087 58.205 48.1607 58.205C41.9127 58.205 36.8467 56.37 36.8467 54.105C36.8467 51.84 41.9137 50.005 48.1607 50.005C54.4077 50.005 59.4747 51.84 59.4747 54.105Z" fill="#E8E8E8"/>
<path d="M56.8343 53.773C56.8343 55.214 52.9513 56.379 48.1613 56.379C43.3713 56.379 39.4883 55.212 39.4883 53.773C39.4883 52.334 43.3723 51.168 48.1613 51.168C52.9503 51.168 56.8343 52.335 56.8343 53.773Z" fill="#50E6FF"/>
<path d="M48.161 54.373C45.832 54.3188 43.5102 54.6531 41.291 55.362C43.5054 56.094 45.8295 56.4381 48.161 56.379C50.4928 56.438 52.8173 56.0939 55.032 55.362C52.8125 54.6531 50.4904 54.3188 48.161 54.373Z" fill="#198AB3"/>
<path d="M55.0376 69.236V63.276H53.3996V70.568H57.7426V69.236H55.0376ZM42.4516 66.3C42.1242 66.163 41.8186 65.9787 41.5446 65.753C41.4712 65.6784 41.4137 65.5897 41.3757 65.4922C41.3377 65.3947 41.32 65.2905 41.3236 65.186C41.3212 65.0809 41.345 64.9769 41.3928 64.8833C41.4407 64.7897 41.5111 64.7096 41.5976 64.65C41.8194 64.5055 42.0814 64.4354 42.3456 64.45C42.9449 64.4411 43.532 64.619 44.0256 64.959V63.439C43.4585 63.2352 42.858 63.1399 42.2556 63.158C41.5614 63.1232 40.877 63.333 40.3216 63.751C40.0878 63.9386 39.9009 64.178 39.7755 64.4503C39.6502 64.7226 39.59 65.0204 39.5996 65.32C39.6295 65.8027 39.8048 66.265 40.1026 66.6461C40.4003 67.0272 40.8065 67.3092 41.2676 67.455C41.6553 67.6177 42.0203 67.8298 42.3536 68.086C42.4376 68.1565 42.505 68.2445 42.5513 68.3438C42.5976 68.4431 42.6216 68.5514 42.6216 68.661C42.624 68.767 42.5997 68.8719 42.5509 68.9661C42.5022 69.0603 42.4306 69.1407 42.3426 69.2C42.1083 69.348 41.8332 69.418 41.5566 69.4C40.8452 69.3956 40.1599 69.1314 39.6296 68.657V70.284C40.2158 70.5731 40.8644 70.7129 41.5176 70.691C42.2542 70.734 42.9846 70.5341 43.5966 70.122C43.8436 69.9355 44.0413 69.6916 44.1725 69.4113C44.3038 69.1311 44.3645 68.8231 44.3496 68.514C44.3638 68.061 44.2095 67.6188 43.9166 67.273C43.4972 66.8553 42.9993 66.5246 42.4516 66.3ZM51.6046 69.1C51.9623 68.4678 52.1622 67.7586 52.1875 67.0327C52.2127 66.3068 52.0625 65.5855 51.7496 64.93C51.4753 64.3788 51.0456 63.9199 50.5136 63.61C49.966 63.297 49.3444 63.1367 48.7136 63.146C48.0448 63.1342 47.3847 63.2998 46.8006 63.626C46.2431 63.9468 45.7924 64.4247 45.5046 65C45.1895 65.6263 45.0316 66.32 45.0446 67.021C45.036 67.664 45.1814 68.2997 45.4686 68.875C45.7381 69.4151 46.1539 69.8687 46.6686 70.184C47.1975 70.5059 47.8017 70.6831 48.4206 70.698L49.9316 72.392H52.0666L49.9526 70.434C50.6424 70.1962 51.2269 69.7242 51.6046 69.1ZM49.9616 68.653C49.8014 68.8546 49.596 69.0155 49.362 69.1229C49.1279 69.2303 48.8719 69.281 48.6146 69.271C48.3571 69.2789 48.1013 69.2251 47.8688 69.1141C47.6362 69.0031 47.4335 68.8382 47.2776 68.633C46.9469 68.1227 46.7717 67.5272 46.7733 66.9191C46.7749 66.311 46.9532 65.7165 47.2866 65.208C47.4479 64.9999 47.656 64.8329 47.8941 64.7206C48.1322 64.6083 48.3935 64.554 48.6566 64.562C48.9127 64.5529 49.167 64.607 49.3973 64.7195C49.6275 64.832 49.8264 64.9994 49.9766 65.207C50.3225 65.7234 50.491 66.3384 50.4566 66.959C50.4936 67.5653 50.3186 68.1655 49.9616 68.657V68.653Z" fill="url(#paint3_radial)"/>
<defs>
<linearGradient id="paint0_linear" x1="57.1865" y1="29.936" x2="70.3875" y2="29.936" gradientUnits="userSpaceOnUse">
<stop stop-color="#005BA1"/>
<stop offset="0.068" stop-color="#0060A9"/>
<stop offset="0.356" stop-color="#0071C8"/>
<stop offset="0.517" stop-color="#0078D4"/>
<stop offset="0.642" stop-color="#0074CD"/>
<stop offset="0.82" stop-color="#006ABB"/>
<stop offset="1" stop-color="#005BA1"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="52.7968" y1="65.192" x2="52.7968" y2="27.156" gradientUnits="userSpaceOnUse">
<stop stop-color="#198AB3"/>
<stop offset="0.097" stop-color="#209EC5"/>
<stop offset="0.242" stop-color="#28B6DA"/>
<stop offset="0.396" stop-color="#2EC7E9"/>
<stop offset="0.565" stop-color="#31D1F2"/>
<stop offset="0.775" stop-color="#32D4F5"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="870.658" y1="18260.5" x2="1382.73" y2="18260.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#005BA1"/>
<stop offset="0.068" stop-color="#0060A9"/>
<stop offset="0.356" stop-color="#0071C8"/>
<stop offset="0.517" stop-color="#0078D4"/>
<stop offset="0.642" stop-color="#0074CD"/>
<stop offset="0.82" stop-color="#006ABB"/>
<stop offset="1" stop-color="#005BA1"/>
</linearGradient>
<radialGradient id="paint3_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(48.7896 67.771) scale(12.478)">
<stop stop-color="#F2F2F2"/>
<stop offset="0.58" stop-color="#EEEEEE"/>
<stop offset="1" stop-color="#E6E6E6"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -2,7 +2,7 @@
"name": "sql-database-projects",
"displayName": "SQL Database Projects",
"description": "The SQL Database Projects extension for Azure Data Studio allows users to develop and publish database schemas.",
"version": "0.5.1",
"version": "0.6.0",
"publisher": "Microsoft",
"preview": true,
"engines": {

View File

@@ -0,0 +1,4 @@
CREATE EXTERNAL DATA SOURCE [@@OBJECT_NAME@@] WITH
(
LOCATION = '@@LOCATION@@'
)

View File

@@ -0,0 +1,5 @@
CREATE EXTERNAL STREAM [@@OBJECT_NAME@@] WITH
(
DATA_SOURCE = [@@DATA_SOURCE_NAME@@],
LOCATION = N'@@LOCATION@@'@@OPTIONS@@
)

View File

@@ -5,6 +5,5 @@
EXEC sys.sp_create_streaming_job @NAME = '@@OBJECT_NAME@@', @STATEMENT = 'INSERT INTO SqlOutputStream SELECT
timeCreated,
streamColumn1 as column1,
streamColumn2 as column2
streamColumn1 as Id
FROM EdgeHubInputStream'

View File

@@ -0,0 +1,5 @@
CREATE EXTERNAL FILE FORMAT [@@OBJECT_NAME@@] WITH
(
FORMAT_TYPE = JSON,
DATA_COMPRESSION = 'org.apache.hadoop.io.compress.GzipCodec'
)

View File

@@ -23,9 +23,13 @@ export const MicrosoftDatatoolsSchemaSqlSql = 'Microsoft.Data.Tools.Schema.Sql.S
export const databaseSchemaProvider = 'DatabaseSchemaProvider';
// Project Provider
export const sqlDatabaseProjectTypeId = 'sqldbproj';
export const projectTypeDisplayName = localize('projectTypeDisplayName', "SQL Database");
export const projectTypeDescription = localize('projectTypeDescription', "Design, edit, and publish schemas for SQL databases");
export const emptySqlDatabaseProjectTypeId = 'EmptySqlDbProj';
export const emptyProjectTypeDisplayName = localize('emptyProjectTypeDisplayName', "SQL Database");
export const emptyProjectTypeDescription = localize('emptyProjectTypeDescription', "Develop and publish schemas for SQL databases starting from an empty project");
export const edgeSqlDatabaseProjectTypeId = 'SqlDbEdgeProj';
export const edgeProjectTypeDisplayName = localize('edgeProjectTypeDisplayName', "SQL Edge");
export const edgeProjectTypeDescription = localize('edgeProjectTypeDescription', "Start with the core pieces to develop and publish schemas for SQL Edge");
// commands
export const revealFileInOsCommand = 'revealFileInOS';
@@ -118,7 +122,7 @@ export const databaseProject = localize('databaseProject', "Database project");
// Create Project From Database dialog strings
export const createProjectFromDatabaseDialogName = localize('createProjectFromDatabaseDialogName', "Create Project From Database");
export const createProjectFromDatabaseDialogName = localize('createProjectFromDatabaseDialogName', "Create project from database");
export const createProjectDialogOkButtonText = localize('createProjectDialogOkButtonText', "Create");
export const sourceDatabase = localize('sourceDatabase', "Source database");
export const targetProject = localize('targetProject', "Target project");
@@ -126,9 +130,13 @@ export const createProjectSettings = localize('createProjectSettings', "Settings
export const projectNameLabel = localize('projectNameLabel', "Name");
export const projectNamePlaceholderText = localize('projectNamePlaceholderText', "Enter project name");
export const projectLocationLabel = localize('projectLocationLabel', "Location");
export const projectLocationPlaceholderText = localize('projectLocationPlaceholderText', "Enter project location");
export const projectLocationPlaceholderText = localize('projectLocationPlaceholderText', "Select location to create project");
export const browseButtonText = localize('browseButtonText', "Browse folder");
export const folderStructureLabel = localize('folderStructureLabel', "Folder structure");
export const addProjectToCurrentWorkspace = localize('addProjectToCurrentWorkspace', "This project will be added to the current workspace.");
export const newWorkspaceWillBeCreated = localize('newWorkspaceWillBeCreated', "A new workspace will be created for this project.");
export const workspaceLocationTitle = localize('workspaceLocationTitle', "Workspace location");
export const workspace = localize('workspace', "Workspace");
// Error messages
@@ -196,6 +204,9 @@ export const scriptFriendlyName = localize('scriptFriendlyName', "Script");
export const tableFriendlyName = localize('tableFriendlyName', "Table");
export const viewFriendlyName = localize('viewFriendlyName', "View");
export const storedProcedureFriendlyName = localize('storedProcedureFriendlyName', "Stored Procedure");
export const dataSourceFriendlyName = localize('dataSource', "Data Source");
export const fileFormatFriendlyName = localize('fileFormat', "File Format");
export const externalStreamFriendlyName = localize('externalStream', "External Stream");
export const externalStreamingJobFriendlyName = localize('externalStreamingJobFriendlyName', "External Streaming Job");
export const preDeployScriptFriendlyName = localize('preDeployScriptFriendlyName', "Script.PreDeployment");
export const postDeployScriptFriendlyName = localize('postDeployScriptFriendlyName', "Script.PostDeployment");

View File

@@ -14,6 +14,7 @@ export class IconPathHelper {
private static extensionContext: vscode.ExtensionContext;
public static databaseProject: IconPath;
public static colorfulSqlProject: IconPath;
public static sqlEdgeProject: IconPath;
public static dataSourceGroup: IconPath;
public static dataSourceSql: IconPath;
@@ -33,6 +34,7 @@ export class IconPathHelper {
IconPathHelper.databaseProject = IconPathHelper.makeIcon('databaseProject');
IconPathHelper.colorfulSqlProject = IconPathHelper.makeIcon('colorfulSqlProject', true);
IconPathHelper.sqlEdgeProject = IconPathHelper.makeIcon('sqlEdgeProject', true);
IconPathHelper.dataSourceGroup = IconPathHelper.makeIcon('dataSourceGroup');
IconPathHelper.dataSourceSql = IconPathHelper.makeIcon('dataSource-sql');

View File

@@ -11,12 +11,15 @@ export namespace cssStyles {
export const fontWeightBold = { 'font-weight': 'bold' };
export const titleFontSize = 13;
export const labelWidth = '205px';
export const textboxWidth = '190px';
export const publishDialogLabelWidth = '205px';
export const publishDialogTextboxWidth = '190px';
export const addDatabaseReferenceDialogLabelWidth = '215px';
export const addDatabaseReferenceInputboxWidth = '220px';
export const createProjectFromDatabaseLabelWidth = '110px';
export const createProjectFromDatabaseTextboxWidth = '320px';
// font-styles
export namespace fontStyle {
export const normal = 'normal';

View File

@@ -56,40 +56,35 @@ export class ProjectsController {
* @param folderUri
* @param projectGuid
*/
public async createNewProject(newProjName: string, folderUri: vscode.Uri, makeOwnFolder: boolean, projectGuid?: string): Promise<string> {
if (projectGuid && !UUID.isUUID(projectGuid)) {
throw new Error(`Specified GUID is invalid: '${projectGuid}'`);
public async createNewProject(creationParams: NewProjectParams): Promise<string> {
if (creationParams.projectGuid && !UUID.isUUID(creationParams.projectGuid)) {
throw new Error(`Specified GUID is invalid: '${creationParams.projectGuid}'`);
}
const macroDict: Record<string, string> = {
'PROJECT_NAME': newProjName,
'PROJECT_GUID': projectGuid ?? UUID.generateUuid().toUpperCase()
'PROJECT_NAME': creationParams.newProjName,
'PROJECT_GUID': creationParams.projectGuid ?? UUID.generateUuid().toUpperCase()
};
let newProjFileContents = this.macroExpansion(templates.newSqlProjectTemplate, macroDict);
let newProjFileContents = templates.macroExpansion(templates.newSqlProjectTemplate, macroDict);
let newProjFileName = newProjName;
let newProjFileName = creationParams.newProjName;
if (!newProjFileName.toLowerCase().endsWith(constants.sqlprojExtension)) {
newProjFileName += constants.sqlprojExtension;
}
const newProjFilePath = makeOwnFolder ? path.join(folderUri.fsPath, path.parse(newProjFileName).name, newProjFileName) : path.join(folderUri.fsPath, newProjFileName);
const newProjFilePath = path.join(creationParams.folderUri.fsPath, path.parse(newProjFileName).name, newProjFileName);
let fileExists = false;
try {
await fs.access(newProjFilePath);
fileExists = true;
}
catch { } // file doesn't already exist
if (fileExists) {
if (await utils.exists(newProjFilePath)) {
throw new Error(constants.projectAlreadyExists(newProjFileName, path.parse(newProjFilePath).dir));
}
await fs.mkdir(path.dirname(newProjFilePath), { recursive: true });
await fs.writeFile(newProjFilePath, newProjFileContents);
await this.addTemplateFiles(newProjFilePath, creationParams.projectTypeId);
return newProjFilePath;
}
@@ -256,7 +251,7 @@ export class ProjectsController {
}
}
const itemType = templates.projectScriptTypeMap()[itemTypeName.toLocaleLowerCase()];
const itemType = templates.get(itemTypeName);
const absolutePathToParent = path.join(project.projectFolderPath, relativePath);
let itemObjectName = await this.promptForNewObjectName(itemType, project, absolutePathToParent, constants.sqlFileExtension);
@@ -266,18 +261,10 @@ export class ProjectsController {
return; // user cancelled
}
const newFileText = this.macroExpansion(itemType.templateScript, { 'OBJECT_NAME': itemObjectName });
const newFileText = templates.macroExpansion(itemType.templateScript, { 'OBJECT_NAME': itemObjectName });
const relativeFilePath = path.join(relativePath, itemObjectName + constants.sqlFileExtension);
try {
// check if file already exists
const absoluteFilePath = path.join(project.projectFolderPath, relativeFilePath);
const fileExists = await utils.exists(absoluteFilePath);
if (fileExists) {
throw new Error(constants.fileAlreadyExists(path.parse(absoluteFilePath).name));
}
const newEntry = await project.addScriptItem(relativeFilePath, newFileText, itemType.type);
await vscode.commands.executeCommand(constants.vscodeOpenCommand, newEntry.fsUri);
@@ -460,6 +447,12 @@ export class ProjectsController {
return addDatabaseReferenceDialog;
}
/**
* Adds a database reference to a project, after selections have been made in the dialog
* @param project project to which to add the database reference
* @param settings settings for the database reference
* @param context a treeItem in a project's hierarchy, to be used to obtain a Project
*/
public async addDatabaseReferenceCallback(project: Project, settings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings | IProjectReferenceSettings, context: dataworkspace.WorkspaceTreeItem): Promise<void> {
try {
if ((<IProjectReferenceSettings>settings).projectName !== undefined) {
@@ -494,6 +487,11 @@ export class ProjectsController {
}
}
/**
* Validates the contents of an external streaming job's query against the last-built dacpac.
* If no dacpac exists at the output path, one will be built first.
* @param node a treeItem in a project's hierarchy, to be used to obtain a Project
*/
public async validateExternalStreamingJob(node: dataworkspace.WorkspaceTreeItem): Promise<mssql.ValidateStreamingJobResult> {
const project: Project = this.getProjectFromContext(node);
@@ -547,6 +545,29 @@ export class ProjectsController {
}
}
private async addTemplateFiles(newProjFilePath: string, projectTypeId: string): Promise<void> {
if (projectTypeId === constants.emptySqlDatabaseProjectTypeId || newProjFilePath === '') {
return;
}
if (projectTypeId === constants.edgeSqlDatabaseProjectTypeId) {
const project = await Project.openProject(newProjFilePath);
await this.createFileFromTemplate(project, templates.get(templates.table), 'DataTable.sql', { 'OBJECT_NAME': 'DataTable' });
await this.createFileFromTemplate(project, templates.get(templates.dataSource), 'EdgeHubInputDataSource.sql', { 'OBJECT_NAME': 'EdgeHubInputDataSource', 'LOCATION': 'edgehub://' });
await this.createFileFromTemplate(project, templates.get(templates.dataSource), 'SqlOutputDataSource.sql', { 'OBJECT_NAME': 'SqlOutputDataSource', 'LOCATION': 'sqlserver://tcp:.,1433' });
await this.createFileFromTemplate(project, templates.get(templates.fileFormat), 'StreamFileFormat.sql', { 'OBJECT_NAME': 'StreamFileFormat' });
await this.createFileFromTemplate(project, templates.get(templates.externalStream), 'EdgeHubInputStream.sql', { 'OBJECT_NAME': 'EdgeHubInputStream', 'DATA_SOURCE_NAME': 'EdgeHubInputDataSource', 'LOCATION': 'input', 'OPTIONS': ',\n\tFILE_FORMAT = StreamFileFormat' });
await this.createFileFromTemplate(project, templates.get(templates.externalStream), 'SqlOutputStream.sql', { 'OBJECT_NAME': 'SqlOutputStream', 'DATA_SOURCE_NAME': 'SqlOutputDataSource', 'LOCATION': 'TSQLStreaming.dbo.DataTable', 'OPTIONS': '' });
await this.createFileFromTemplate(project, templates.get(templates.externalStreamingJob), 'EdgeStreamingJob.sql', { 'OBJECT_NAME': 'EdgeStreamingJob' });
}
}
private async createFileFromTemplate(project: Project, itemType: templates.ProjectScriptType, relativePath: string, expansionMacros: Record<string, string>): Promise<void> {
const newFileText = templates.macroExpansion(itemType.templateScript, expansionMacros);
await project.addScriptItem(relativePath, newFileText, itemType.type);
}
private getProjectFromContext(context: Project | BaseProjectTreeItem | dataworkspace.WorkspaceTreeItem): Project {
if ('element' in context) {
return context.element.root.project;
@@ -570,21 +591,7 @@ export class ProjectsController {
return (ext.exports as mssql.IExtension).dacFx;
}
private macroExpansion(template: string, macroDict: Record<string, string>): string {
const macroIndicator = '@@';
let output = template;
for (const macro in macroDict) {
// check if value contains the macroIndicator, which could break expansion for successive macros
if (macroDict[macro].includes(macroIndicator)) {
throw new Error(`Macro value ${macroDict[macro]} is invalid because it contains ${macroIndicator}`);
}
output = output.replace(new RegExp(macroIndicator + macro + macroIndicator, 'g'), macroDict[macro]);
}
return output;
}
private async promptForNewObjectName(itemType: templates.ProjectScriptType, _project: Project, folderPath: string, fileExtension?: string): Promise<string | undefined> {
const suggestedName = itemType.friendlyName.replace(/\s+/g, '');
@@ -630,21 +637,29 @@ export class ProjectsController {
try {
const workspaceApi = utils.getDataWorkspaceExtensionApi();
const newProjFolderUri = model.filePath;
const validateWorkspace = await workspaceApi.validateWorkspace();
if (validateWorkspace) {
const newProjFolderUri = model.filePath;
const newProjFilePath = await this.createNewProject(model.projName, vscode.Uri.file(newProjFolderUri), true);
model.filePath = path.dirname(newProjFilePath);
this.setFilePath(model);
const newProjFilePath = await this.createNewProject({
newProjName: model.projName,
folderUri: vscode.Uri.file(newProjFolderUri),
projectTypeId: constants.emptySqlDatabaseProjectTypeId
});
const project = await Project.openProject(newProjFilePath);
await this.createProjectFromDatabaseApiCall(model); // Call ExtractAPI in DacFx Service
let fileFolderList: string[] = model.extractTarget === mssql.ExtractTarget.file ? [model.filePath] : await this.generateList(model.filePath); // Create a list of all the files and directories to be added to project
model.filePath = path.dirname(newProjFilePath);
this.setFilePath(model);
await project.addToProject(fileFolderList); // Add generated file structure to the project
const project = await Project.openProject(newProjFilePath);
await this.createProjectFromDatabaseApiCall(model); // Call ExtractAPI in DacFx Service
let fileFolderList: string[] = model.extractTarget === mssql.ExtractTarget.file ? [model.filePath] : await this.generateList(model.filePath); // Create a list of all the files and directories to be added to project
// add project to workspace
workspaceApi.showProjectsView();
await workspaceApi.addProjectsToWorkspace([vscode.Uri.file(newProjFilePath)]);
await project.addToProject(fileFolderList); // Add generated file structure to the project
// add project to workspace
workspaceApi.showProjectsView();
await workspaceApi.addProjectsToWorkspace([vscode.Uri.file(newProjFilePath)]);
}
}
catch (err) {
vscode.window.showErrorMessage(utils.getErrorMessage(err));
@@ -718,3 +733,10 @@ export class ProjectsController {
//#endregion
}
export interface NewProjectParams {
newProjName: string;
folderUri: vscode.Uri;
projectTypeId: string;
projectGuid?: string;
}

View File

@@ -8,6 +8,7 @@ import * as vscode from 'vscode';
import * as constants from '../common/constants';
import * as newProjectTool from '../tools/newProjectTool';
import * as mssql from '../../../mssql';
import * as path from 'path';
import { IconPathHelper } from '../common/iconHelper';
import { cssStyles } from '../common/uiConstants';
@@ -24,6 +25,7 @@ export class CreateProjectFromDatabaseDialog {
public projectNameTextBox: azdata.InputBoxComponent | undefined;
public projectLocationTextBox: azdata.InputBoxComponent | undefined;
public folderStructureDropDown: azdata.DropDownComponent | undefined;
public workspaceInputBox: azdata.InputBoxComponent | undefined;
private formBuilder: azdata.FormBuilder | undefined;
private connectionId: string | undefined;
private toDispose: vscode.Disposable[] = [];
@@ -81,6 +83,10 @@ export class CreateProjectFromDatabaseDialog {
const createProjectSettingsFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
createProjectSettingsFormSection.addItems([folderStructureRow]);
const workspaceContainerRow = this.createWorkspaceContainerRow(view);
const createworkspaceContainerFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
createworkspaceContainerFormSection.addItems([workspaceContainerRow]);
this.formBuilder = <azdata.FormBuilder>view.modelBuilder.formContainer()
.withFormItems([
{
@@ -106,13 +112,22 @@ export class CreateProjectFromDatabaseDialog {
component: createProjectSettingsFormSection,
}
]
},
{
title: constants.workspace,
components: [
{
component: createworkspaceContainerFormSection,
}
]
}
], {
horizontal: false,
titleFontSize: cssStyles.titleFontSize
})
.withLayout({
width: '100%'
width: '100%',
padding: '10px 10px 0 20px'
});
let formModel = this.formBuilder.component();
@@ -129,12 +144,11 @@ export class CreateProjectFromDatabaseDialog {
const serverLabel = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: constants.server,
requiredIndicator: true,
width: cssStyles.labelWidth,
CSSStyles: cssStyles.fontWeightBold
width: cssStyles.createProjectFromDatabaseLabelWidth
}).component();
const connectionRow = view.modelBuilder.flexContainer().withItems([serverLabel, sourceConnectionTextBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-bottom': '-10px', 'margin-top': '-20px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
connectionRow.insertItem(selectConnectionButton, 2, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '-10px', 'margin-top': '-20px' } });
const connectionRow = view.modelBuilder.flexContainer().withItems([serverLabel, sourceConnectionTextBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-bottom': '-5px', 'margin-top': '-10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
connectionRow.insertItem(selectConnectionButton, 2, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '-5px', 'margin-top': '-10px' } });
return connectionRow;
}
@@ -143,21 +157,21 @@ export class CreateProjectFromDatabaseDialog {
this.sourceDatabaseDropDown = view.modelBuilder.dropDown().withProperties({
ariaLabel: constants.databaseNameLabel,
required: true,
width: cssStyles.textboxWidth,
width: cssStyles.createProjectFromDatabaseTextboxWidth,
editable: true,
fireOnTextChange: true
}).component();
this.sourceDatabaseDropDown.onValueChanged(() => {
this.setProjectName();
this.updateWorkspaceInputbox(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!);
this.tryEnableCreateButton();
});
const databaseLabel = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: constants.databaseNameLabel,
requiredIndicator: true,
width: cssStyles.labelWidth,
CSSStyles: cssStyles.fontWeightBold
width: cssStyles.createProjectFromDatabaseLabelWidth
}).component();
const databaseRow = view.modelBuilder.flexContainer().withItems([databaseLabel, <azdata.DropDownComponent>this.sourceDatabaseDropDown], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-bottom': '-10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
@@ -173,7 +187,7 @@ export class CreateProjectFromDatabaseDialog {
this.sourceConnectionTextBox = view.modelBuilder.inputBox().withProperties({
value: '',
placeHolder: constants.selectConnection,
width: cssStyles.textboxWidth,
width: cssStyles.createProjectFromDatabaseTextboxWidth,
enabled: false
}).component();
@@ -229,24 +243,26 @@ export class CreateProjectFromDatabaseDialog {
private createProjectNameRow(view: azdata.ModelView): azdata.FlexContainer {
this.projectNameTextBox = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: constants.projectNamePlaceholderText,
placeHolder: constants.projectNamePlaceholderText,
required: true,
width: cssStyles.textboxWidth,
width: cssStyles.createProjectFromDatabaseTextboxWidth,
validationErrorMessage: constants.projectNameRequired
}).component();
this.projectNameTextBox.onTextChanged(() => {
this.projectNameTextBox!.value = this.projectNameTextBox!.value?.trim();
this.projectNameTextBox!.updateProperty('title', this.projectNameTextBox!.value);
this.updateWorkspaceInputbox(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!);
this.tryEnableCreateButton();
});
const projectNameLabel = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: constants.projectNameLabel,
requiredIndicator: true,
width: cssStyles.labelWidth,
CSSStyles: cssStyles.fontWeightBold
width: cssStyles.createProjectFromDatabaseLabelWidth
}).component();
const projectNameRow = view.modelBuilder.flexContainer().withItems([projectNameLabel, this.projectNameTextBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-bottom': '-10px', 'margin-top': '-20px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
const projectNameRow = view.modelBuilder.flexContainer().withItems([projectNameLabel, this.projectNameTextBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-bottom': '-5px', 'margin-top': '-10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
return projectNameRow;
}
@@ -258,20 +274,20 @@ export class CreateProjectFromDatabaseDialog {
value: '',
ariaLabel: constants.projectLocationLabel,
placeHolder: constants.projectLocationPlaceholderText,
width: cssStyles.textboxWidth,
width: cssStyles.createProjectFromDatabaseTextboxWidth,
validationErrorMessage: constants.projectLocationRequired
}).component();
this.projectLocationTextBox.onTextChanged(() => {
this.projectLocationTextBox!.updateProperty('title', this.projectLocationTextBox!.value);
this.updateWorkspaceInputbox(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!);
this.tryEnableCreateButton();
});
const projectLocationLabel = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: constants.projectLocationLabel,
requiredIndicator: true,
width: cssStyles.labelWidth,
CSSStyles: cssStyles.fontWeightBold
width: cssStyles.createProjectFromDatabaseLabelWidth
}).component();
const projectLocationRow = view.modelBuilder.flexContainer().withItems([projectLocationLabel, this.projectLocationTextBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-bottom': '-10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
@@ -302,6 +318,7 @@ export class CreateProjectFromDatabaseDialog {
this.projectLocationTextBox!.value = folderUris[0].fsPath;
this.projectLocationTextBox!.updateProperty('title', folderUris[0].fsPath);
this.updateWorkspaceInputbox(this.projectLocationTextBox!.value!, this.projectNameTextBox!.value!);
});
return browseFolderButton;
@@ -313,7 +330,7 @@ export class CreateProjectFromDatabaseDialog {
value: constants.schemaObjectType,
ariaLabel: constants.folderStructureLabel,
required: true,
width: cssStyles.textboxWidth
width: cssStyles.createProjectFromDatabaseTextboxWidth
}).component();
this.folderStructureDropDown.onValueChanged(() => {
@@ -323,15 +340,50 @@ export class CreateProjectFromDatabaseDialog {
const folderStructureLabel = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: constants.folderStructureLabel,
requiredIndicator: true,
width: cssStyles.labelWidth,
CSSStyles: cssStyles.fontWeightBold
width: cssStyles.createProjectFromDatabaseLabelWidth
}).component();
const folderStructureRow = view.modelBuilder.flexContainer().withItems([folderStructureLabel, <azdata.DropDownComponent>this.folderStructureDropDown], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-top': '-20px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
const folderStructureRow = view.modelBuilder.flexContainer().withItems([folderStructureLabel, <azdata.DropDownComponent>this.folderStructureDropDown], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-top': '-10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
return folderStructureRow;
}
/**
* Creates container with information on which workspace the project will be added to and where the workspace will be
* created if no workspace is currently open
* @param view
*/
private createWorkspaceContainerRow(view: azdata.ModelView): azdata.FlexContainer {
this.workspaceInputBox = view.modelBuilder.inputBox().withProperties({
ariaLabel: constants.workspaceLocationTitle,
enabled: false,
value: vscode.workspace.workspaceFile?.fsPath ?? '',
title: vscode.workspace.workspaceFile?.fsPath ?? '' // hovertext for if file path is too long to be seen in textbox
}).component();
const workspaceLabel = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: vscode.workspace.workspaceFile ? constants.addProjectToCurrentWorkspace : constants.newWorkspaceWillBeCreated,
CSSStyles: { 'margin-top': '-10px', 'margin-bottom': '5px' }
}).component();
const workspaceContainerRow = view.modelBuilder.flexContainer().withItems([workspaceLabel, this.workspaceInputBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px', 'margin-top': '0px' } }).withLayout({ flexFlow: 'column' }).component();
return workspaceContainerRow;
}
/**
* Update the workspace inputbox based on the passed in location and name if there isn't a workspace currently open
* @param location
* @param name
*/
public updateWorkspaceInputbox(location: string, name: string): void {
if (!vscode.workspace.workspaceFile) {
const fileLocation = location && name ? path.join(location, `${name}.code-workspace`) : '';
this.workspaceInputBox!.value = fileLocation;
this.workspaceInputBox!.title = fileLocation;
}
}
// only enable Create button if all fields are filled
public tryEnableCreateButton(): void {
if (this.sourceConnectionTextBox!.value && this.sourceDatabaseDropDown!.value

View File

@@ -289,7 +289,7 @@ export class PublishDatabaseDialog {
value: '',
ariaLabel: constants.targetConnectionLabel,
placeHolder: constants.selectConnection,
width: cssStyles.textboxWidth,
width: cssStyles.publishDialogTextboxWidth,
enabled: false
}).component();
@@ -353,12 +353,12 @@ export class PublishDatabaseDialog {
this.loadProfileTextBox = view.modelBuilder.inputBox().withProperties({
placeHolder: constants.loadProfilePlaceholderText,
ariaLabel: constants.profile,
width: cssStyles.textboxWidth
width: cssStyles.publishDialogTextboxWidth
}).component();
const profileLabel = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: constants.profile,
width: cssStyles.labelWidth
width: cssStyles.publishDialogLabelWidth
}).component();
const profileRow = view.modelBuilder.flexContainer().withItems([profileLabel, this.loadProfileTextBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
@@ -374,7 +374,7 @@ export class PublishDatabaseDialog {
const serverLabel = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: constants.server,
requiredIndicator: true,
width: cssStyles.labelWidth
width: cssStyles.publishDialogLabelWidth
}).component();
const connectionRow = view.modelBuilder.flexContainer().withItems([serverLabel, this.targetConnectionTextBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
@@ -389,7 +389,7 @@ export class PublishDatabaseDialog {
value: this.getDefaultDatabaseName(),
ariaLabel: constants.databaseNameLabel,
required: true,
width: cssStyles.textboxWidth,
width: cssStyles.publishDialogTextboxWidth,
editable: true,
fireOnTextChange: true
}).component();
@@ -401,7 +401,7 @@ export class PublishDatabaseDialog {
const databaseLabel = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: constants.databaseNameLabel,
requiredIndicator: true,
width: cssStyles.labelWidth
width: cssStyles.publishDialogLabelWidth
}).component();
const databaseRow = view.modelBuilder.flexContainer().withItems([databaseLabel, <azdata.DropDownComponent>this.targetDatabaseDropDown], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();

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