Compare commits

..

91 Commits

Author SHA1 Message Date
Karl Burtram
4d4917d328 Bump version for 1.25.2 release (#14007) 2021-01-20 14:05:08 -08:00
Hale Rankin
edea311757 Revised section scrolling logic to fix broken user experience. (#13926) (#14005) 2021-01-20 13:55:16 -08:00
Karl Burtram
e7eacc32c0 Bump ADS version for Hotfix (#13761) 2020-12-10 13:19:22 -08:00
Charles Gagnon
12f50cca8d Update STS to revert SqlClient update (#13758) (#13760)
(cherry picked from commit 94feb1a80d)
2020-12-10 13:08:03 -08:00
Charles Gagnon
88a4dba695 [Port] Fix env var names for Arc deployment (#13735)
* Fix environment variables for controller create (#13732)


(cherry picked from commit aee8bc2759)

* vBump and update engine version
2020-12-09 10:50:16 -08:00
Charles Gagnon
634ea0ab6a Use console.log for retry logging (#13722) (#13723)
(cherry picked from commit a74119038f)
2020-12-08 10:28:02 -08:00
Charles Gagnon
cd0b5cbc7a Retry getConfig (#13712) (#13713)
* Retry getConfig

* Add logging

(cherry picked from commit d6e1e8eb52)
2020-12-07 15:14:38 -08:00
Charles Gagnon
0b7de6608a Retry publish and always try adding asset (#13700) (#13704)
* Retry publish and always try adding asset

* Undo asset upload change

* Add logging

(cherry picked from commit 6c89c61b0d)
2020-12-07 12:36:03 -08:00
Charles Gagnon
8c6bd8c857 [Port] Add descriptions/validations for Arc connected mode deployment (#13689)
* Add descriptions and validation to connected mode (#13676)


(cherry picked from commit 757ac1d4aa)

* bump version
2020-12-04 16:52:39 -08:00
Monica Gupta
def5775e00 Fix issue with pasting results in Teams (#13673) (#13687)
* Fix issue with pasting results in Teams

* Addressed comment to change header tag to th

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

Co-authored-by: Monica Gupta <mogupt@microsoft.com>
2020-12-04 16:31:08 -08:00
Chris LaFreniere
91da9aea98 Prevent Table from Disappearing due to exception when looking for tHead (#13680) (#13685)
* Prevent exception when tHead doesn't exist at node

* Add test for no thead
2020-12-04 15:14:32 -08:00
Vasu Bhog
1c898402f8 Fix notebook unordered grid values after papermill execution (#13614) (#13665)
* 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 18:10:24 -08:00
Monica Gupta
54210cf479 Fix empty column issue (#13641) (#13653)
Co-authored-by: Monica Gupta <mogupt@microsoft.com>

Co-authored-by: Monica Gupta <mogupt@microsoft.com>
2020-12-03 14:57:16 -08:00
Barbara Valdez
cbcea87a82 add right padding to notebook toolbar action item (#13640) (#13650)
* add right padding to action item

* remove extra line and add space
2020-12-03 11:30:10 -08:00
Barbara Valdez
2d50d2c5d1 add await to thenable method (#13635) (#13638) 2020-12-02 18:12:48 -08:00
Chris LaFreniere
7448c6c32c WYSIWYG Improvements to highlight (#13032) (#13636)
* Improvements to highlight

* wip

* Tests pass

* Leverage escaping mechanism

* Tweak highlight logic

* PR comments
2020-12-02 16:26:18 -08:00
Karl Burtram
3196cf5be0 Bump distro to pickup new icons (#13598) (#13625) 2020-12-02 11:24:23 -08:00
Kim Santiago
666726a5fa Update open existing dialog icons (#13571) (#13593)
* update open existing dialog icons

* undo removing folder.svg

* remove max width and max height
2020-12-01 14:38:06 -08:00
Benjin Dubishar
818a3d204e Update tools service to .61 (#13591) (#13595) 2020-12-01 14:36:01 -08:00
Lucy Zhang
d45758b4f4 dont add column header in continue request (#13568) (#13577) 2020-11-30 15:12:06 -08:00
Benjin Dubishar
1eda5eb33a Adding additional parameter to data workspace provider API (#13570) (#13574) 2020-11-30 13:50:31 -08:00
Sakshi Sharma
6ac5b7c8a5 Add Import UI to Data-workspace Ellipsis (#13544) 2020-11-30 08:48:09 -08:00
Monica Gupta
397354ebc3 Copy clipboard command in ADS (html/plain text supported) (#13527)
* draft commit

* few changes

* Changes to copy query with results in plain and html formatting

* undo changes

* undo unintended change

* remove comments

* Addressed comments

* Some clean up

Co-authored-by: Monica Gupta <mogupt@microsoft.com>
2020-11-25 21:08:29 -08:00
Vasu Bhog
2a7b90fd70 Fix WYSIWYG text + image paste (#13542)
* Fix WYSIWYG text + image paste

* add test for a link and text
2020-11-25 12:42:07 -06:00
Lucy Zhang
1554e51932 Add logs to smoke tests (#13533)
* add logs

* fix log option

* update log folder
2020-11-25 06:58:43 -08:00
Arvind Ranasaria
d060f1b9a0 Classes for adding kube config and kube cluster picker to Controller connection dialog (#13479) 2020-11-24 20:08:27 -08:00
Alan Ren
c8632c255a scoped refresh commands (#13541) 2020-11-24 17:32:50 -08:00
Charles Gagnon
2ac03b9ef4 Fix error path for predict button on ML import (#13540) 2020-11-24 16:08:26 -08:00
Charles Gagnon
a0d89449cc Fix BDC table icons (#13539) 2020-11-24 15:36:22 -08:00
Charles Gagnon
b03a914934 Update required icon for labels for dynamic enablement (#13515) 2020-11-24 15:05:29 -08:00
Barbara Valdez
d3bcb942f5 Add synapse repo to dropdown option (#13536)
* Add synapse repo

* fix typo
2020-11-24 15:01:34 -08:00
Charles Gagnon
84822b23ac Fix BDC Deployment table (#13538)
* Fix BDC Deployment table

* fix mappings

* Fix names
2020-11-24 14:28:19 -08:00
Charles Gagnon
40ca82c63d Fix declarative table display issues with ML ext (#13529)
* Fix declarative table display issues with ML ext

* Fix test
2020-11-24 12:55:04 -08:00
Udeesha Gautam
f4a6b42b3a Adding ML and DB project to Recommended Extns (#13526)
* adding DB Project to recommended extensions

* adding ML extensions to recommended extensions
2020-11-24 12:09:40 -08:00
Alex Ma
7ad631d4a5 assets folder removed (#13525) 2020-11-24 11:02:32 -08:00
Maddy
4b7baa652f update css to remove extra padding (#13491) 2020-11-24 10:47:52 -08:00
Alan Ren
148e802f4a show the resources as soon as they become available (#13530)
* results streaming

* remove variable
2020-11-24 10:45:14 -08:00
Charles Gagnon
7bb4d00073 Let child ModelView components control their own enabled status by default (#13524) 2020-11-23 15:49:02 -08:00
Leila Lali
3b20e8a61c Fixed an issue when azure account is expired or not valid (#13483) 2020-11-23 13:03:50 -08:00
Alan Ren
6e0a4f27de fix the icon sizing issue (#13522) 2020-11-23 13:02:52 -08:00
Charles Gagnon
21ddf30a7b Increase head size for sql script compile (#13520) 2020-11-23 10:59:44 -08:00
Alex Ma
2ade45858e Removal of placeholder notebooks in Hybrid Toolkit Extension (#13505)
* placeholder notebooks removed and readme changed

* toc updated as well
2020-11-23 10:43:41 -08:00
Vladimir Chernov
e0b1a3460d bumping versions and using ComponentWithIcon props to set icon size (#13517)
* bumping versions and using ComponentWithIcon props to set icon size

* combine withProps sections
2020-11-23 21:41:45 +03:00
Charles Gagnon
f44c714cf2 Update SqlToolsService to .59 (#13519) 2020-11-23 10:29:19 -08:00
Kim Santiago
f72e12fe32 Move focus to inside sql database projects dialogs when they open (#13512) 2020-11-23 10:06:35 -08:00
Alan Ren
0b6fb504dc fix icon size issue (#13514) 2020-11-20 21:31:13 -08:00
Charles Gagnon
145b2491df Cleanup Resource Deployment ModelView (#13510) 2020-11-20 18:29:00 -08:00
Lucy Zhang
aa30b52d03 Notebooks: Fix query results not displaying table rows (#13488)
* fix PQSQL queries not displaying rows

* comment

* change comment and fix unit test
2020-11-20 15:31:01 -08:00
Charles Gagnon
6edcbbb738 Suppress scan warnings (#13507) 2020-11-20 14:28:57 -08:00
Alan Ren
815c61315c add option to show link icon (#13506) 2020-11-20 14:06:07 -08:00
Charles Gagnon
172a044ba7 Remove inputValueTransformer and getInputComponentValue (#13502) 2020-11-20 10:51:00 -08:00
Vladimir Chernov
2a81a0a70f Sql-Assessment api info button (#13490) 2020-11-20 20:53:34 +03:00
Sakshi Sharma
749989cd0b Create project from database UI dialog (#13179)
* UI hook up

* Add tests

* Add back the missing statement for opening project

* Fix failures

* Add a few more tests

* Fix test failure

* Addressed comments

* Update UI to match the mocks

* Update UI to match updated mockups

* Addressed comments to match UI with mockup

* Updated all import strings to be called as Create Project From Database strings

* Fix a couple of test failures and one comment addressed

* Update one missed import string

* Skipping a failing test for now

* Fix failures. Fix alignment of icons

* Addressed PR comments

* Addressed couple more PR comments
2020-11-20 09:38:16 -08:00
Chris LaFreniere
8d42182db8 Attempt to Colorize Code Cells from Notebook Contents (#13473)
* Attempt to colorize from saved language info

* Simplify colorization change

* Fixup
2020-11-19 19:54:09 -08:00
Charles Gagnon
bb2a1db6e8 Add connectivity mode option to Arc controller create (#13495)
* Add connectivity mode option to Arc controller create

* Add connectivity mode to summary

* Use name instead of display name for dropdown values
2020-11-19 17:06:49 -08:00
Kim Santiago
c579ecb111 update sql-database-projects and schema compare versions (#13489) 2020-11-19 16:25:16 -08:00
Charles Gagnon
175d46d508 Add support for dynamic enablement of resource deployment components (#13464)
* saving wip to merge main

* temp fixes for textValidation* removal

* save wip to switch tasks

* save wip to switch tasks

* save wip to switch tasks

* code complete - with known bugs

* missed test file

* fix extHostModelView changes

* validation module

* missed test changes

* missed change

* pr feedback

* pr feedback

* revert inadvertent change

* remove unneeded change

* merge from bug/12082-2

* pr feedback

* pr feedback

* bdd -> tdd for validation tests

* bdd -> tdd for validation tests

* pr feedback

* remove unneeded file

* pr feedback

* EOL instead of '\n'

* pr feedback

* pr feedback

* minor fixes.

* pr feedback

* fix comment

* comments and var renames

* test fixes

* working version after validation simplification

* working version after validation simplification

* remove inadvertent change

* simplified validations

* undo uneeded change

* cleanup

* working version after latest merge

* comments and whitespace fixes

* remove is_integer checks

* sentence case field validation messages

* Use generic strings in sample fields

* minor fixes to sample extension strings

* spaces to tabs for indentation

* request fields before limit fields

* reaarange request/limit fields

* is_integer checks for PG Server Group number fields

* Thenable to Promise

* InputBoxInfo

* pr feedback

* pr feedback

* isUndefinedOrEmpty to utils

* include asde package.json

* use ValidationValueType

* ValidationValueType -> InputValueType

* Add support for dynamic enablement of resource deployment components

* use instanceof function

* getValue returns InputValueType

Co-authored-by: Arvind Ranasaria <ranasaria@outlook.com>
2020-11-19 16:23:28 -08:00
Leila Lali
870ff39527 Fixed the issue with adding and removing flex container (#13480) 2020-11-19 09:19:39 -08:00
Charles Gagnon
ddd0b8b4bc Remove pandas import from some notebooks (#13481) 2020-11-19 08:19:20 -08:00
Arvind Ranasaria
c7cca5afea Improved Validations for ARC Wizards (#12945) 2020-11-18 22:03:59 -08:00
Kim Santiago
e63e4f0901 update a few strings in projects (#13482) 2020-11-18 20:04:33 -08:00
Kim Santiago
ddc8c00090 Data workspace projects changes (#13466)
* Fix project context menu actions (#12541)

* delete works again

* make fewer changes

* update all sql db project commands

* cleanup

* Remove old projects view (#12563)

* remove old projects view from file explorer view

* fix tests failing

* remove projects in open folder opening up in old view

* Update db reference dialog to show projects in the workspace (#12580)

* update database reference dialog to show projects in the workspace in the project dropdown

* remove workspace stuff from sql projects extension

* undo change

* add class that implements IExtension

* undo a change

* update DataWorkspaceExtension to take workspaceService as a parameter

* add type

* Update sql database project commands (#12595)

* remove sql proj's open and create new project from comman palette

* hook up create project from database to data workspace

* rename the remaining import databases to create project from database

* remove open, new, and close commands

* expose addProjectsToWorkspace() in IExtension instead of calling command

* Addressing comments

* fix failing sql project tests (#12651)

* update SSDT projects opened in projects viewlet (#12669)

* fix action not refreshing the tree issue (#12692)

* fix adding project references in new projects viewlet (#12688)

* Remove old projects tree provider (#12702)

* Remove old projects tree provider and fix tests

* formatting

* update refreshProjectsTree() to accept workspaceTreeItem()

* Cleanup ProjectsController (#12718)

* remove openProject from ProjectController and some cleanup

* rename

* add project and open project dialogs (#12729)

* empty dialogs

* wip

* new project dialog implementation

* revert gitattributes

* open project dialog

* implement add project

* remove icon helper

* refactor

* revert script change

* adjust views

* more updates

* make data-workspace a builtin extension

* show the view only when project provider is detected (#12819)

* only show the view when proj provider is available

* update

* fix sql project tests after merge (#12793)

* Update dialogs to be closer to mockups (#12879)

* small UI changes to dialogs

* center radio card group text

* Create workspace if needed when opening/new project (#12930)

* empty dialogs

* wip

* new project dialog implementation

* revert gitattributes

* open project dialog

* implement add project

* remove icon helper

* refactor

* revert script change

* create workspace

* initial changes

* create new workspace working

* fix tests

* cleanup

* remove showWorkspaceRequiredNotification()

* Add test for no workspace open

* update blue buttons

* move loading temp project to activate() instead of workspaceService constructor

* move workspace creation warning message to before project is created

* pass uri to createWorkspace

* add tests

Co-authored-by: Alan Ren <alanren@microsoft.com>

* Additional create workspace changes (#13004)

* Dialogs workspace updates (#13010)

* adding workspace text boxes

* match new project dialog to mockups

* Add validation error message for workspace file

* add enterWorkspace api

* add warning message for opening workspace

* cleanup

* update commands to remove project so they're more generic

* remove 'empty' from string

* Move default project location setting to data workspace extension (#13022)

* remove project location setting and notification from sql database projects extension

* add default project location setting to data workspace extension

* fix typo

* Add back project name incrementing

* other merge fixes

* fix strings from other PR

* default to last opened directory instead of home directory if no specified default location

* A few small updates (#13092)

* fix build error

* update title for inputboxes

* add missing file

* Add tests for data workspace dialogs (#13324)

* add tests for dialogs

* create helper functions

* New project dialog workspace inputbox fixes (#13407)

* workspace inputbox fixes

* fix folder icons

* Update package.jsons and readme (#13451)

* update package.jsons

* update readme

* add workspace information to open existing dialog (#13455)

Co-authored-by: Alan Ren <alanren@microsoft.com>
2020-11-18 16:13:43 -08:00
Charles Gagnon
34170e7741 Fix Loading component removal (#13478)
* Fix Loading component removal

* More undefined checks
2020-11-18 15:42:33 -08:00
Karl Burtram
f5e4b32d01 Update VM notebook 2020-11-18 15:16:40 -08:00
Kim Santiago
28fef53731 fix schema compare database dropdown not starting with values (#13461) 2020-11-17 13:42:00 -08:00
Aasim Khan
438bc67072 Fixed the generate script logic for notebook wizards. (#13418)
* Fixed the generate script logic for notebook wizards.

* -reverted previous changes
-added last page check to the page validation change logic.

* checking if the page is valid when entering it.

* removing unnecessary index variable in forEach loop

* added comments for generate script button enabling on notebookwizard page.
2020-11-17 10:26:45 -08:00
Charles Gagnon
472c9decfa Display error when doing notebook convert (#13438)
* Display error when doing notebook convert

* Update STS
2020-11-17 10:03:33 -08:00
Charles Gagnon
6cd2d6c942 Fix extension install version check (#13436) 2020-11-17 09:58:50 -08:00
Arvind Ranasaria
39e1181c5d Remove WizardBase.ts (#13350) 2020-11-16 19:21:26 -08:00
Lucy Zhang
c898b50b94 Remove resultSet from IDisplayResult metadata (#13450)
* remove resultSet from IDisplayResult metadata

* remove metadata from IDisplayResult
2020-11-16 17:54:09 -08:00
Justin M
271fe62344 12567 Fixed Notebooks not adding to recent connections (#13113)
* 12567 Changed tryAddActiveConnection to always add recent connection

* 12567 Reverted change to tryAddActiveConnection. Removed this._params.input from connectionDialogService > createModel

* 12567 Simplified conditional in connectionDialogService
2020-11-16 11:27:09 -08:00
Justin M
c18a54bc1d 12666 Passed azureAccount into onFetchDatabases and set on tempProfile. (#13239) 2020-11-16 11:24:22 -08:00
Charles Gagnon
b57bf53b67 Fix ModelView container child layout issues (#13412) 2020-11-16 10:59:21 -08:00
Charles Gagnon
c699179e15 Re-register contributed book commands when new extension loaded (#13403) 2020-11-16 10:47:27 -08:00
Alan Ren
690937443c enable the outline for active tab header (#13415) 2020-11-16 10:21:30 -08:00
Kim Santiago
698b79f0f3 bump sql database projects version (#13408) 2020-11-14 11:10:14 -08:00
Alan Ren
798af5fc2d uncomment the hideContextMenu (#13411) 2020-11-13 16:30:17 -08:00
Charles Gagnon
af55dcfb42 Convert ModelView validate to Promise (#13390)
* Convert ModelView validate to Promise

* more cleanup
2020-11-13 15:31:22 -08:00
Charles Gagnon
76781d6cf4 Fix ModelView logging error (#13410) 2020-11-13 14:40:00 -08:00
Alan Ren
99e3da5b48 Editable dropdown component improvement (#13389)
* replace Tree with List

* comments
2020-11-13 13:36:54 -08:00
Chris LaFreniere
6b657259a5 Ensure that we close editors before utils tests (#13391) 2020-11-13 11:16:14 -08:00
Charles Gagnon
cbe2ba0901 Add some more logging to ModelView components (#13387)
* Add some more logging to ModelView components

* Remove catch

* remove unused
2020-11-13 10:30:40 -08:00
Arvind Ranasaria
b3d99117ca onComplete -> onLeave for toolsAndEulaPage (#13394) 2020-11-13 09:59:08 -08:00
Charles Gagnon
32ac586431 Add MIAA Compute+Storage page (#13367)
* Add MIAA Compute+Storage page

* update min memory limits

* update strings

* feedback
2020-11-13 07:20:46 -08:00
Aasim Khan
bd4676ac8c removing separate heading for page number and clubbing it with wizard title (#13380) 2020-11-13 00:38:04 -08:00
Charles Gagnon
536628603e Fix ModelView container validation ordering (#13386)
* Fix ModelView container validation ordering

* Also validate component after adding

* undo child component validate call
2020-11-12 17:17:27 -08:00
Kim Santiago
ea7fe08b98 remove sqlproj from Project tree node (#13379) 2020-11-12 15:49:34 -08:00
Aasim Khan
6c920f6d54 Fixed the table width so it does not overflow from the wizard. (#13372) 2020-11-12 15:06:03 -08:00
Alex Ma
a2f7136728 Update for Azure SQL Hybrid Cloud Toolkit (#13360)
* Added azurehybridtoolkit to list of external extensions

* Added updated book

* added to recommended extensions

* extensions.js updated for build

* added small changes to extension

* small changes to extension

* tsconfig change

* gitignore and vscode changes

* changed package display name
2020-11-12 14:22:50 -08:00
Charles Gagnon
a082c1e478 Ignore built notebook component files (#13369) 2020-11-12 13:17:54 -08:00
Lucy Zhang
32a6385fef Notebooks: Save cell connection name in cell metadata (#13208)
* save connection info in notebook metadata

* update attachTo dropdown based on saved alias

* add setting for saving connection (default=false)

* save/read cell connection name to/from metadata

* get started on toggling multi connection mode

* add activeConnection property to cell model

* add changeContext method for cell

* add comments

* add unit test for reading connection name

* save connection mode in metadata

* clean up code

* address PR comments
2020-11-12 10:44:34 -08:00
278 changed files with 6146 additions and 3606 deletions

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

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

View File

@@ -213,9 +213,9 @@ const externalExtensions = [
'arc',
'asde-deployment',
'azdata',
'azurehybridtoolkit',
'cms',
'dacpac',
'data-workspace',
'import',
'kusto',
'liveshare',

View File

@@ -247,9 +247,9 @@ const externalExtensions = [
'arc',
'asde-deployment',
'azdata',
'azurehybridtoolkit',
'cms',
'dacpac',
'data-workspace',
'import',
'kusto',
'liveshare',

View File

@@ -65,13 +65,7 @@
{
"cell_type": "code",
"source": [
"import pandas,sys,os,json,html,getpass,time, tempfile\n",
"pandas_version = pandas.__version__.split('.')\n",
"pandas_major = int(pandas_version[0])\n",
"pandas_minor = int(pandas_version[1])\n",
"pandas_patch = int(pandas_version[2])\n",
"if not (pandas_major > 0 or (pandas_major == 0 and pandas_minor > 24) or (pandas_major == 0 and pandas_minor == 24 and pandas_patch >= 2)):\n",
" sys.exit('Please upgrade the Notebook dependency before you can proceed, you can do it by running the \"Reinstall Notebook dependencies\" command in command palette (View menu -> Command Palette…).')\n",
"import sys,os,json,html,getpass,time, tempfile\n",
"def run_command(command):\n",
" print(\"Executing: \" + command)\n",
" !{command}\n",
@@ -138,7 +132,13 @@
" sys.exit(f'Password is required.')\n",
" confirm_password = getpass.getpass(prompt = 'Confirm password')\n",
" if arc_admin_password != confirm_password:\n",
" sys.exit(f'Passwords do not match.')"
" sys.exit(f'Passwords do not match.')\n",
"\n",
"os.environ[\"SPN_CLIENT_ID\"] = sp_client_id\n",
"os.environ[\"SPN_TENANT_ID\"] = sp_tenant_id\n",
"if \"AZDATA_NB_VAR_SP_CLIENT_SECRET\" in os.environ:\n",
" os.environ[\"SPN_CLIENT_SECRET\"] = os.environ[\"AZDATA_NB_VAR_SP_CLIENT_SECRET\"]\n",
"os.environ[\"SPN_AUTHORITY\"] = \"https://login.microsoftonline.com\""
],
"metadata": {
"azdata_cell_guid": "e7e10828-6cae-45af-8c2f-1484b6d4f9ac",
@@ -188,7 +188,7 @@
"os.environ[\"AZDATA_PASSWORD\"] = arc_admin_password\n",
"if os.name == 'nt':\n",
" print(f'If you don\\'t see output produced by azdata, you can run the following command in a terminal window to check the deployment status:\\n\\t {os.environ[\"AZDATA_NB_VAR_KUBECTL\"]} get pods -n {arc_data_controller_namespace}')\n",
"run_command(f'azdata arc dc create --connectivity-mode Indirect -n {arc_data_controller_name} -ns {arc_data_controller_namespace} -s {arc_subscription} -g {arc_resource_group} -l {arc_data_controller_location} -sc {arc_data_controller_storage_class} --profile-name {arc_profile}')\n",
"run_command(f'azdata arc dc create --connectivity-mode {arc_data_controller_connectivity_mode} -n {arc_data_controller_name} -ns {arc_data_controller_namespace} -s {arc_subscription} -g {arc_resource_group} -l {arc_data_controller_location} -sc {arc_data_controller_storage_class} --profile-name {arc_profile}')\n",
"print(f'Azure Arc Data Controller: {arc_data_controller_name} created.') "
],
"metadata": {

View File

@@ -2,14 +2,14 @@
"name": "arc",
"displayName": "%arc.displayName%",
"description": "%arc.description%",
"version": "0.6.3",
"version": "0.6.5",
"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",
@@ -137,7 +137,11 @@
"description": "%resource.type.azure.arc.description%",
"platforms": "*",
"icon": "./images/data_controller.svg",
"tags": ["Hybrid", "SQL Server", "PostgreSQL"],
"tags": [
"Hybrid",
"SQL Server",
"PostgreSQL"
],
"providers": [
{
"notebookWizard": {
@@ -196,7 +200,7 @@
]
},
{
"title": "%arc.data.controller.data.controller.create.title%",
"title": "%arc.data.controller.create.azureconfig.title%",
"sections": [
{
"title": "%arc.data.controller.project.details.title%",
@@ -210,53 +214,14 @@
"type": "azure_account",
"required": true,
"subscriptionVariableName": "AZDATA_NB_VAR_ARC_SUBSCRIPTION",
"displaySubscriptionVariableName": "AZDATA_NB_VAR_ARC_DISPLAY_SUBSCRIPTION",
"resourceGroupVariableName": "AZDATA_NB_VAR_ARC_RESOURCE_GROUP"
}
]
},
{
"title": "%arc.data.controller.data.controller.details.title%",
"fields": [
{
"type": "readonly_text",
"label": "%arc.data.controller.data.controller.details.description%",
"labelWidth": "600px"
},
{
"type": "text",
"label": "%arc.data.controller.arc.data.controller.namespace%",
"textValidationRequired": true,
"textValidationRegex": "^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$",
"textValidationDescription": "%arc.data.controller.arc.data.controller.namespace.validation.description%",
"defaultValue": "arc",
"required": true,
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE"
},
{
"type": "text",
"label": "%arc.data.controller.arc.data.controller.name%",
"textValidationRequired": true,
"textValidationRegex": "^[a-z0-9]([-.a-z0-9]{0,251}[a-z0-9])?$",
"textValidationDescription": "%arc.data.controller.arc.data.controller.name.validation.description%",
"defaultValue": "arc-dc",
"required": true,
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME"
},
{
"label": "%arc.storage-class.dc.label%",
"description": "%arc.sql.storage-class.dc.description%",
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_STORAGE_CLASS",
"type": "kube_storage_class",
"required": true
},
{
"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",
"displayLocationVariableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_DISPLAY_LOCATION",
"locations": [
"australiaeast",
"centralus",
@@ -274,6 +239,141 @@
}
]
},
{
"title": "%arc.data.controller.connectivitymode%",
"fields": [
{
"type": "readonly_text",
"label": "%arc.data.controller.connectivitymode.description%",
"labelWidth": "600px"
},
{
"type": "options",
"label": "%arc.data.controller.connectivitymode%",
"required": true,
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE",
"options": {
"values": [
{
"name": "indirect",
"displayName": "%arc.data.controller.indirect%"
},
{
"name": "direct",
"displayName": "%arc.data.controller.direct%"
}
],
"defaultValue": "%arc.data.controller.indirect%",
"optionsType": "radio"
}
},
{
"type": "readonly_text",
"label": "%arc.data.controller.serviceprincipal.description%",
"labelWidth": "600px",
"links": [
{
"text": "%arc.data.controller.readmore%",
"url": "https://docs.microsoft.com/azure/azure-arc/data/upload-metrics"
}
]
},
{
"label": "%arc.data.controller.spclientid%",
"description": "%arc.data.controller.spclientid.description%",
"variableName": "AZDATA_NB_VAR_SP_CLIENT_ID",
"type": "text",
"required": true,
"defaultValue": "",
"placeHolder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"enabled": {
"target": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE",
"value": "direct"
},
"validations" : [{
"type": "regex_match",
"regex": "^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$",
"description": "%arc.data.controller.spclientid.validation.description%"
}]
},
{
"label": "%arc.data.controller.spclientsecret%",
"description": "%arc.data.controller.spclientsecret.description%",
"variableName": "AZDATA_NB_VAR_SP_CLIENT_SECRET",
"type": "password",
"required": true,
"defaultValue": "",
"enabled": {
"target": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE",
"value": "direct"
}
},
{
"label": "%arc.data.controller.sptenantid%",
"description": "%arc.data.controller.sptenantid.description%",
"variableName": "AZDATA_NB_VAR_SP_TENANT_ID",
"type": "text",
"required": true,
"defaultValue": "",
"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.sptenantid.validation.description%"
}]
}
]
}
]
},
{
"title": "%arc.data.controller.create.controllerconfig.title%",
"sections": [
{
"title": "%arc.data.controller.details.title%",
"fields": [
{
"type": "readonly_text",
"label": "%arc.data.controller.details.description%",
"labelWidth": "600px"
},
{
"type": "text",
"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.namespace.validation.description%"
}],
"defaultValue": "arc",
"required": true,
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE"
},
{
"type": "text",
"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.name.validation.description%"
}],
"defaultValue": "arc-dc",
"required": true,
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME"
},
{
"label": "%arc.storage-class.dc.label%",
"description": "%arc.sql.storage-class.dc.description%",
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_STORAGE_CLASS",
"type": "kube_storage_class",
"required": true
}
]
},
{
"title": "%arc.data.controller.admin.account.title%",
"fields": [
@@ -300,7 +400,7 @@
]
},
{
"title": "%arc.data.controller.data.controller.create.summary.title%",
"title": "%arc.data.controller.create.summary.title%",
"isSummaryPage": true,
"fieldHeight": "16px",
"sections": [
@@ -470,7 +570,7 @@
"label": "%arc.data.controller.summary.subscription%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DISPLAY_SUBSCRIPTION)",
"defaultValue": "$(AZDATA_NB_VAR_ARC_SUBSCRIPTION)",
"inputWidth": "600"
},
{
@@ -483,7 +583,18 @@
"label": "%arc.data.controller.summary.location%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_DISPLAY_LOCATION)"
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_LOCATION)"
}
]
},
{
"title": "%arc.data.controller.summary.controller%",
"fields": [
{
"label": "%arc.data.controller.connectivitymode%",
"type": "readonly_text",
"isEvaluated": true,
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE)"
}
]
}
@@ -510,7 +621,10 @@
"description": "%resource.type.arc.sql.description%",
"platforms": "*",
"icon": "./images/miaa.svg",
"tags": ["Hybrid", "SQL Server"],
"tags": [
"Hybrid",
"SQL Server"
],
"providers": [
{
"notebookWizard": {
@@ -559,18 +673,22 @@
"type": "text",
"defaultValue": "sqlinstance1",
"required": true,
"textValidationRequired": true,
"textValidationRegex": "^[a-z]([-a-z0-9]{0,11}[a-z0-9])?$",
"textValidationDescription": "%arc.sql.invalid.instance.name%"
"validations" : [{
"type": "regex_match",
"regex": "^[a-z]([-a-z0-9]{0,11}[a-z0-9])?$",
"description": "%arc.sql.invalid.instance.name%"
}]
},
{
"label": "%arc.sql.username%",
"variableName": "AZDATA_NB_VAR_SQL_USERNAME",
"type": "text",
"required": true,
"textValidationRequired": true,
"textValidationRegex": "^(?!sa$)",
"textValidationDescription": "%arc.sql.invalid.username%"
"validations" : [{
"type": "regex_match",
"regex": "^(?!sa$)",
"description": "%arc.sql.invalid.username%"
}]
},
{
"label": "%arc.password%",
@@ -607,7 +725,14 @@
"variableName": "AZDATA_NB_VAR_SQL_CORES_REQUEST",
"type": "number",
"min": 1,
"required": false
"required": false,
"validations": [
{
"type": "<=",
"target": "AZDATA_NB_VAR_SQL_CORES_LIMIT",
"description": "%requested.cores.less.than.or.equal.to.cores.limit%"
}
]
},
{
"label": "%arc.cores-limit.label%",
@@ -615,7 +740,14 @@
"variableName": "AZDATA_NB_VAR_SQL_CORES_LIMIT",
"type": "number",
"min": 1,
"required": false
"required": false,
"validations": [
{
"type": ">=",
"target": "AZDATA_NB_VAR_SQL_CORES_REQUEST",
"description": "%cores.limit.greater.than.or.equal.to.requested.cores%"
}
]
},
{
"label": "%arc.memory-request.label%",
@@ -623,7 +755,12 @@
"variableName": "AZDATA_NB_VAR_SQL_MEMORY_REQUEST",
"type": "number",
"min": 2,
"required": false
"required": false,
"validations": [{
"type": "<=",
"target": "AZDATA_NB_VAR_SQL_MEMORY_LIMIT",
"description": "%requested.memory.less.than.or.equal.to.memory.limit%"
}]
},
{
"label": "%arc.memory-limit.label%",
@@ -631,7 +768,12 @@
"variableName": "AZDATA_NB_VAR_SQL_MEMORY_LIMIT",
"type": "number",
"min": 2,
"required": false
"required": false,
"validations": [{
"type": ">=",
"target": "AZDATA_NB_VAR_SQL_MEMORY_REQUEST",
"description": "%memory.limit.greater.than.or.equal.to.requested.memory%"
}]
}
]
}
@@ -671,7 +813,10 @@
"description": "%resource.type.arc.postgres.description%",
"platforms": "*",
"icon": "./images/postgres.svg",
"tags": ["Hybrid", "PostgreSQL"],
"tags": [
"Hybrid",
"PostgreSQL"
],
"providers": [
{
"notebookWizard": {
@@ -719,9 +864,11 @@
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME",
"type": "text",
"description": "%arc.postgres.server.group.name.validation.description%",
"textValidationRequired": true,
"textValidationRegex": "^[a-z]([-a-z0-9]{0,10}[a-z0-9])?$",
"textValidationDescription": "%arc.postgres.server.group.name.validation.description%",
"validations" : [{
"type": "regex_match",
"regex": "^[a-z]([-a-z0-9]{0,10}[a-z0-9])?$",
"description": "%arc.postgres.server.group.name.validation.description%"
}],
"required": true
},
{
@@ -738,6 +885,10 @@
"description": "%arc.postgres.server.group.workers.description%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_WORKERS",
"type": "number",
"validations": [{
"type": "is_integer",
"description": "%should.be.integer%"
}],
"defaultValue": "0",
"min": 0
},
@@ -745,6 +896,10 @@
"label": "%arc.postgres.server.group.port%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PORT",
"type": "number",
"validations": [{
"type": "is_integer",
"description": "%should.be.integer%"
}],
"defaultValue": "5432",
"min": 1,
"max": 65535
@@ -825,28 +980,48 @@
"description": "%arc.postgres.server.group.cores.request.description%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_REQUEST",
"type": "number",
"min": 1
"min": 1,
"validations": [{
"type": "<=",
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_LIMIT",
"description": "%requested.cores.less.than.or.equal.to.cores.limit%"
}]
},
{
"label": "%arc.postgres.server.group.cores.limit.label%",
"description": "%arc.postgres.server.group.cores.limit.description%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_LIMIT",
"type": "number",
"min": 1
"min": 1,
"validations": [{
"type": ">=",
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_REQUEST",
"description": "%cores.limit.greater.than.or.equal.to.requested.cores%"
}]
},
{
"label": "%arc.postgres.server.group.memory.request.label%",
"description": "%arc.postgres.server.group.memory.request.description%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_REQUEST",
"type": "number",
"min": 0.25
"min": 0.25,
"validations": [{
"type": "<=",
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_LIMIT",
"description": "%requested.memory.less.than.or.equal.to.memory.limit%"
}]
},
{
"label": "%arc.postgres.server.group.memory.limit.label%",
"description": "%arc.postgres.server.group.memory.limit.description%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_LIMIT",
"type": "number",
"min": 0.25
"min": 0.25,
"validations": [{
"type": ">=",
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_REQUEST",
"description": "%memory.limit.greater.than.or.equal.to.requested.memory%"
}]
}
]
}
@@ -885,7 +1060,8 @@
"dependencies": {
"request": "^2.88.0",
"uuid": "^8.3.0",
"vscode-nls": "^4.1.2"
"vscode-nls": "^4.1.2",
"yamljs": "^0.3.0"
},
"devDependencies": {
"@types/mocha": "^5.2.5",
@@ -893,6 +1069,7 @@
"@types/request": "^2.48.3",
"@types/sinon": "^9.0.4",
"@types/uuid": "^8.3.0",
"@types/yamljs": "^0.2.31",
"mocha": "^5.2.0",
"mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7",

View File

@@ -20,21 +20,35 @@
"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.title": "Provide details to create Azure Arc data controller",
"arc.data.controller.project.details.title": "Project details",
"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 an Azure region and a name for your Azure Arc data controller. This name will be used to identify your Arc location 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",
"arc.data.controller.admin.account.confirm.password": "Confirm password",
"arc.data.controller.data.controller.create.summary.title": "Review your configuration",
"arc.data.controller.connectivitymode": "Connectivity Mode",
"arc.data.controller.direct": "Direct",
"arc.data.controller.indirect": "Indirect",
"arc.data.controller.serviceprincipal.description": "When deploying a controller in direct connected mode a Service Principal is required for 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",
@@ -55,8 +69,10 @@
"arc.data.controller.summary.resource.group": "Resource group",
"arc.data.controller.summary.data.controller.name": "Data controller name",
"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",
@@ -129,6 +145,11 @@
"arc.postgres.server.group.memory.limit.label": "Memory limit (GB per node)",
"arc.postgres.server.group.memory.limit.description": "The memory limit of the Postgres instance per node in GB.",
"arc.agreement": "I accept {0} and {1}.",
"arc.agreement.sql.terms.conditions":"Azure SQL managed instance - Azure Arc terms and conditions",
"arc.agreement.postgres.terms.conditions":"Azure Arc enabled PostgreSQL Hyperscale terms and conditions"
"arc.agreement.sql.terms.conditions": "Azure SQL managed instance - Azure Arc terms and conditions",
"arc.agreement.postgres.terms.conditions": "Azure Arc enabled PostgreSQL Hyperscale terms and conditions",
"should.be.integer": "Value must be an integer",
"requested.cores.less.than.or.equal.to.cores.limit": "Requested cores must be less than or equal to cores limit",
"cores.limit.greater.than.or.equal.to.requested.cores": "Cores limit must be greater than or equal to requested cores",
"requested.memory.less.than.or.equal.to.memory.limit": "Requested memory must be less than or equal to memory limit",
"memory.limit.greater.than.or.equal.to.requested.memory": "Memory limit must be greater than or equal to requested memory"
}

View File

@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import * as path from 'path';
import * as yamljs from 'yamljs';
import * as loc from '../localizedConstants';
import { throwUnless } from './utils';
export interface KubeClusterContext {
name: string;
isCurrentContext: boolean;
}
export function getKubeConfigClusterContexts(configFile: string): Promise<KubeClusterContext[]> {
const config: any = yamljs.load(configFile);
const rawContexts = <any[]>config['contexts'];
throwUnless(rawContexts && rawContexts.length, loc.noContextFound(configFile));
const currentContext = <string>config['current-context'];
throwUnless(currentContext, loc.noCurrentContextFound(configFile));
const contexts: KubeClusterContext[] = [];
rawContexts.forEach(rawContext => {
const name = <string>rawContext['name'];
throwUnless(name, loc.noNameInContext(configFile));
if (name) {
contexts.push({
name: name,
isCurrentContext: name === currentContext
});
}
});
return Promise.resolve(contexts);
}
export function getDefaultKubeConfigPath(): string {
return path.join(os.homedir(), '.kube', 'config');
}

View File

@@ -67,7 +67,7 @@ export function getResourceTypeIcon(resourceType: string | undefined): IconPath
/**
* Returns the text to display for known connection modes
* @param connectionMode The string repsenting the connection mode
* @param connectionMode The string representing the connection mode
*/
export function getConnectionModeDisplayText(connectionMode: string | undefined): string {
connectionMode = connectionMode ?? '';
@@ -282,8 +282,18 @@ export function convertToGibibyteString(value: string): string {
* @param condition
* @param message
*/
export function throwUnless(condition: boolean, message?: string): asserts condition {
export function throwUnless(condition: any, message?: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
export async function tryExecuteAction<T>(action: () => T | PromiseLike<T>): Promise<{ result: T | undefined, error: any }> {
let error: any, result: T | undefined;
try {
result = await action();
} catch (e) {
error = e;
}
return { result, error };
}

View File

@@ -63,7 +63,7 @@ export const feedback = localize('arc.feedback', "Feedback");
export const selectConnectionString = localize('arc.selectConnectionString', "Select from available client connection strings below.");
export const addingWokerNodes = localize('arc.addingWokerNodes', "adding worker nodes");
export const workerNodesDescription = localize('arc.workerNodesDescription', "Expand your server group and scale your database by adding worker nodes.");
export const configurationInformation = localize('arc.configurationInformation', "You can configure the number of CPU cores and storage size that will apply to both worker nodes and coordinator node. Each worker node will have the same configuration. Adjust the number of CPU cores and memory settings for your server group.");
export const postgresConfigurationInformation = localize('arc.postgres.configurationInformation', "You can configure the number of CPU cores and storage size that will apply to both worker nodes and coordinator node. Each worker node will have the same configuration. Adjust the number of CPU cores and memory settings for your server group.");
export const workerNodesInformation = localize('arc.workerNodeInformation', "In preview it is not possible to reduce the number of worker nodes. Please refer to documentation linked above for more information.");
export const vCores = localize('arc.vCores', "vCores");
export const ram = localize('arc.ram', "RAM");
@@ -121,8 +121,9 @@ export const enterNewPassword = localize('arc.enterNewPassword', "Enter a new pa
export const confirmNewPassword = localize('arc.confirmNewPassword', "Confirm the new password");
export const learnAboutPostgresClients = localize('arc.learnAboutPostgresClients', "Learn more about Azure PostgreSQL Hyperscale client interfaces");
export const scalingCompute = localize('arc.scalingCompute', "scaling compute vCores and memory.");
export const computeAndStorageDescriptionPartOne = localize('arc.computeAndStorageDescriptionPartOne', "You can scale your Azure Arc enabled");
export const computeAndStorageDescriptionPartTwo = localize('arc.computeAndStorageDescriptionPartTwo', "PostgreSQL Hyperscale server group by");
export const postgresComputeAndStorageDescriptionPartOne = localize('arc.postgresComputeAndStorageDescriptionPartOne', "You can scale your Azure Arc enabled");
export const miaaComputeAndStorageDescriptionPartOne = localize('arc.miaaComputeAndStorageDescriptionPartOne', "You can scale your Azure SQL managed instance - Azure Arc by");
export const postgresComputeAndStorageDescriptionPartTwo = localize('arc.postgres.computeAndStorageDescriptionPartTwo', "PostgreSQL Hyperscale server group by");
export const computeAndStorageDescriptionPartThree = localize('arc.computeAndStorageDescriptionPartThree', "without downtime and by");
export const computeAndStorageDescriptionPartFour = localize('arc.computeAndStorageDescriptionPartFour', "Before doing so, you need to ensure");
export const computeAndStorageDescriptionPartFive = localize('arc.computeAndStorageDescriptionPartFive', "there are sufficient resources available");
@@ -201,3 +202,6 @@ export const variableValueFetchForUnsupportedVariable = (variableName: string) =
export const isPasswordFetchForUnsupportedVariable = (variableName: string) => localize('getIsPassword.unknownVariableName', "Attempt to get isPassword for unknown variable:{0}", variableName);
export const noControllerInfoFound = (name: string) => localize('noControllerInfoFound', "Controller Info could not be found with name: {0}", name);
export const noPasswordFound = (controllerName: string) => localize('noPasswordFound', "Password could not be retrieved for controller: {0} and user did not provide a password. Please retry later.", controllerName);
export const 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);

View File

@@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as path from 'path';
import * as sinon from 'sinon';
import * as yamljs from 'yamljs';
import { getDefaultKubeConfigPath, getKubeConfigClusterContexts, KubeClusterContext } from '../../common/kubeUtils';
import { tryExecuteAction } from '../../common/utils';
const kubeConfig =
{
'contexts': [
{
'context': {
'cluster': 'docker-desktop',
'user': 'docker-desktop'
},
'name': 'docker-for-desktop'
},
{
'context': {
'cluster': 'kubernetes',
'user': 'kubernetes-admin'
},
'name': 'kubernetes-admin@kubernetes'
}
],
'current-context': 'docker-for-desktop'
};
describe('KubeUtils', function (): void {
const configFile = 'kubeConfig';
afterEach('KubeUtils cleanup', () => {
sinon.restore();
});
it('getDefaultKubeConfigPath', async () => {
getDefaultKubeConfigPath().should.endWith(path.join('.kube', 'config'));
});
describe('get Kube Config Cluster Contexts', () => {
it('success', async () => {
sinon.stub(yamljs, 'load').returns(<any>kubeConfig);
const verifyContexts = (contexts: KubeClusterContext[], testName: string) => {
contexts.length.should.equal(2, `test: ${testName} failed`);
contexts[0].name.should.equal('docker-for-desktop', `test: ${testName} failed`);
contexts[0].isCurrentContext.should.be.true(`test: ${testName} failed`);
contexts[1].name.should.equal('kubernetes-admin@kubernetes', `test: ${testName} failed`);
contexts[1].isCurrentContext.should.be.false(`test: ${testName} failed`);
};
verifyContexts(await getKubeConfigClusterContexts(configFile), 'getKubeConfigClusterContexts');
});
it('throws error when unable to load config file', async () => {
const error = new Error('unknown error accessing file');
sinon.stub(yamljs, 'load').throws(error); //erroring config file load
((await tryExecuteAction(() => getKubeConfigClusterContexts(configFile))).error).should.equal(error, `test: getKubeConfigClusterContexts failed`);
});
});
});

View File

@@ -56,7 +56,16 @@ export class FakeAzdataApi implements azdataExt.IAzdataApi {
mi: {
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
async list(): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> { return <any>{ result: self.miaaInstances }; },
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> { throw new Error('Method not implemented.'); }
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> { throw new Error('Method not implemented.'); },
edit(
_name: string,
_args: {
coresLimit?: string,
coresRequest?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean
}): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
}
}
};

View File

@@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
export class FakeRadioButton implements azdata.RadioButtonComponent {
private _onDidClickEmitter = new vscode.EventEmitter<any>();
onDidClick = this._onDidClickEmitter.event;
constructor(props: azdata.RadioButtonProperties) {
this.label = props.label;
this.value = props.value;
this.checked = props.checked;
this.enabled = props.enabled;
}
//#region RadioButtonProperties implementation
label?: string;
value?: string;
checked?: boolean;
//#endregion
click() {
this.checked = true;
this._onDidClickEmitter.fire(this);
}
//#region Component Implementation
id: string = '';
updateProperties(_properties: { [key: string]: any; }): Thenable<void> {
throw new Error('Method not implemented.');
}
updateProperty(_key: string, _value: any): Thenable<void> {
throw new Error('Method not implemented.');
}
updateCssStyles(_cssStyles: { [key: string]: string; }): Thenable<void> {
throw new Error('Method not implemented.');
}
onValidityChanged: vscode.Event<boolean> = <vscode.Event<boolean>>{};
valid: boolean = false;
validate(): Thenable<boolean> {
throw new Error('Method not implemented.');
}
focus(): Thenable<void> {
throw new Error('Method not implemented.');
}
ariaHidden?: boolean | undefined;
//#endregion
//#region ComponentProperties Implementation
height?: number | string;
width?: number | string;
/**
* The position CSS property. Empty by default.
* This is particularly useful if laying out components inside a FlexContainer and
* the size of the component is meant to be a fixed size. In this case the position must be
* set to 'absolute', with the parent FlexContainer having 'relative' position.
* Without this the component will fail to correctly size itself
*/
position?: azdata.PositionType;
/**
* Whether the component is enabled in the DOM
*/
enabled?: boolean;
/**
* Corresponds to the display CSS property for the element
*/
display?: azdata.DisplayType;
/**
* Corresponds to the aria-label accessibility attribute for this component
*/
ariaLabel?: string;
/**
* Corresponds to the role accessibility attribute for this component
*/
ariaRole?: string;
/**
* Corresponds to the aria-selected accessibility attribute for this component
*/
ariaSelected?: boolean;
/**
* Matches the CSS style key and its available values.
*/
CSSStyles?: { [key: string]: string };
//#endregion
}

View File

@@ -43,7 +43,7 @@ describe('ControllerModel', function (): void {
});
it('Reads password from cred store', async function (): Promise<void> {
const password = 'password123';
const password = 'password123'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Test password, not actually used")]
// Set up cred store to return our password
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();

View File

@@ -3,8 +3,66 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as TypeMoq from 'typemoq';
import * as vscode from 'vscode';
export function createModelViewMock() {
const mockModelBuilder = TypeMoq.Mock.ofType<azdata.ModelBuilder>();
const mockTextBuilder = setupMockComponentBuilder<azdata.TextComponent, azdata.TextComponentProperties>();
const mockInputBoxBuilder = setupMockComponentBuilder<azdata.InputBoxComponent, azdata.InputBoxProperties>();
const mockRadioButtonBuilder = setupMockComponentBuilder<azdata.RadioButtonComponent, azdata.RadioButtonProperties>();
const mockDivBuilder = setupMockContainerBuilder<azdata.DivContainer, azdata.DivContainerProperties, azdata.DivBuilder>();
const mockLoadingBuilder = setupMockLoadingBuilder();
mockModelBuilder.setup(b => b.loadingComponent()).returns(() => mockLoadingBuilder.object);
mockModelBuilder.setup(b => b.text()).returns(() => mockTextBuilder.object);
mockModelBuilder.setup(b => b.inputBox()).returns(() => mockInputBoxBuilder.object);
mockModelBuilder.setup(b => b.radioButton()).returns(() => mockRadioButtonBuilder.object);
mockModelBuilder.setup(b => b.divContainer()).returns(() => mockDivBuilder.object);
const mockModelView = TypeMoq.Mock.ofType<azdata.ModelView>();
mockModelView.setup(mv => mv.modelBuilder).returns(() => mockModelBuilder.object);
return { mockModelView, mockModelBuilder, mockTextBuilder, mockInputBoxBuilder, mockRadioButtonBuilder, mockDivBuilder };
}
function setupMockLoadingBuilder(
loadingBuilderGetter?: (item: azdata.Component) => azdata.LoadingComponentBuilder,
mockLoadingBuilder?: TypeMoq.IMock<azdata.LoadingComponentBuilder>
): TypeMoq.IMock<azdata.LoadingComponentBuilder> {
mockLoadingBuilder = mockLoadingBuilder ?? setupMockComponentBuilder<azdata.LoadingComponent, azdata.LoadingComponentProperties, azdata.LoadingComponentBuilder>();
let item: azdata.Component;
mockLoadingBuilder.setup(b => b.withItem(TypeMoq.It.isAny())).callback((_item) => item = _item).returns(() => loadingBuilderGetter ? loadingBuilderGetter(item) : mockLoadingBuilder!.object);
return mockLoadingBuilder;
}
export function setupMockComponentBuilder<T extends azdata.Component, P extends azdata.ComponentProperties, B extends azdata.ComponentBuilder<T, P> = azdata.ComponentBuilder<T, P>>(
componentGetter?: (props: P) => T,
mockComponentBuilder?: TypeMoq.IMock<B>,
): TypeMoq.IMock<B> {
mockComponentBuilder = mockComponentBuilder ?? TypeMoq.Mock.ofType<B>();
const returnComponent = TypeMoq.Mock.ofType<T>();
// Need to setup 'then' for when a mocked object is resolved otherwise the test will hang : https://github.com/florinn/typemoq/issues/66
returnComponent.setup((x: any) => x.then).returns(() => { });
let compProps: P;
mockComponentBuilder.setup(b => b.withProperties(TypeMoq.It.isAny())).callback((props: P) => compProps = props).returns(() => mockComponentBuilder!.object);
mockComponentBuilder.setup(b => b.component()).returns(() => {
return componentGetter ? componentGetter(compProps) : Object.assign<T, P>(Object.assign({}, returnComponent.object), compProps);
});
// For now just have these be passthrough - can hook up additional functionality later if needed
mockComponentBuilder.setup(b => b.withValidation(TypeMoq.It.isAny())).returns(() => mockComponentBuilder!.object);
return mockComponentBuilder;
}
export function setupMockContainerBuilder<T extends azdata.Container<any, any>, P extends azdata.ComponentProperties, B extends azdata.ContainerBuilder<T, any, any, any> = azdata.ContainerBuilder<T, any, any, any>>(
mockContainerBuilder?: TypeMoq.IMock<B>
): TypeMoq.IMock<B> {
mockContainerBuilder = mockContainerBuilder ?? setupMockComponentBuilder<T, P, B>();
// For now just have these be passthrough - can hook up additional functionality later if needed
mockContainerBuilder.setup(b => b.withItems(TypeMoq.It.isAny(), undefined)).returns(() => mockContainerBuilder!.object);
mockContainerBuilder.setup(b => b.withLayout(TypeMoq.It.isAny())).returns(() => mockContainerBuilder!.object);
return mockContainerBuilder;
}
export class MockInputBox implements vscode.InputBox {
private _value: string = '';
public get value(): string {

View File

@@ -0,0 +1,88 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as should from 'should';
import { getErrorMessage } from '../../../common/utils';
import { RadioOptionsGroup, RadioOptionsInfo } from '../../../ui/components/radioOptionsGroup';
import { FakeRadioButton } from '../../mocks/fakeRadioButton';
import { setupMockComponentBuilder, createModelViewMock } from '../../stubs';
const loadingError = new Error('Error loading options');
const radioOptionsInfo = <RadioOptionsInfo>{
values: [
'value1',
'value2'
],
defaultValue: 'value2'
};
const divItems: azdata.Component[] = [];
let radioOptionsGroup: RadioOptionsGroup;
describe('radioOptionsGroup', function (): void {
beforeEach(async () => {
const { mockModelView, mockRadioButtonBuilder, mockDivBuilder } = createModelViewMock();
mockRadioButtonBuilder.reset(); // reset any previous mock so that we can set our own.
setupMockComponentBuilder<azdata.RadioButtonComponent, azdata.RadioButtonProperties>(
(props) => new FakeRadioButton(props),
mockRadioButtonBuilder,
);
mockDivBuilder.reset(); // reset previous setups so new setups we are about to create will replace the setups instead creating a recording chain
// create new setups for the DivContainer with custom behavior
setupMockComponentBuilder<azdata.DivContainer, azdata.DivContainerProperties, azdata.DivBuilder>(
() => <azdata.DivContainer>{
addItem: (item) => { divItems.push(item); },
clearItems: () => { divItems.length = 0; },
get items() { return divItems; },
},
mockDivBuilder
);
radioOptionsGroup = new RadioOptionsGroup(mockModelView.object, (_disposable) => { });
await radioOptionsGroup.load(async () => radioOptionsInfo);
});
it('verify construction and load', async () => {
should(radioOptionsGroup).not.be.undefined();
should(radioOptionsGroup.value).not.be.undefined();
radioOptionsGroup.value!.should.equal('value2', 'radio options group should be the default checked value');
// verify all the radioButtons created in the group
verifyRadioGroup();
});
it('onClick', async () => {
// click the radioButton corresponding to 'value1'
(divItems as FakeRadioButton[]).filter(r => r.value === 'value1').pop()!.click();
radioOptionsGroup.value!.should.equal('value1', 'radio options group should correspond to the radioButton that we clicked');
// verify all the radioButtons created in the group
verifyRadioGroup();
});
it('load throws', async () => {
radioOptionsGroup.load(() => { throw loadingError; });
//in error case radioButtons array wont hold radioButtons but holds a TextComponent with value equal to error string
divItems.length.should.equal(1, 'There is should be only one element in the divContainer when loading error happens');
const label = divItems[0] as azdata.TextComponent;
should(label.value).not.be.undefined();
label.value!.should.deepEqual(getErrorMessage(loadingError));
should(label.CSSStyles).not.be.undefined();
should(label.CSSStyles!.color).not.be.undefined();
label.CSSStyles!.color.should.equal('Red');
});
});
function verifyRadioGroup() {
const radioButtons = divItems as FakeRadioButton[];
radioButtons.length.should.equal(radioOptionsInfo.values!.length);
radioButtons.forEach(rb => {
should(rb.label).not.be.undefined();
should(rb.value).not.be.undefined();
should(rb.enabled).not.be.undefined();
rb.label!.should.equal(rb.value);
rb.enabled!.should.be.true();
});
}

View File

@@ -0,0 +1,72 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { getErrorMessage } from '../../common/utils';
export interface RadioOptionsInfo {
values?: string[],
defaultValue: string
}
export class RadioOptionsGroup {
static id: number = 1;
private _divContainer!: azdata.DivContainer;
private _loadingBuilder: azdata.LoadingComponentBuilder;
private _currentRadioOption!: azdata.RadioButtonComponent;
constructor(private _view: azdata.ModelView, private _onNewDisposableCreated: (disposable: vscode.Disposable) => void, private _groupName: string = `RadioOptionsGroup${RadioOptionsGroup.id++}`) {
const divBuilder = this._view.modelBuilder.divContainer();
const divBuilderWithProperties = divBuilder.withProperties<azdata.DivContainerProperties>({ clickable: false });
this._divContainer = divBuilderWithProperties.component();
const loadingComponentBuilder = this._view.modelBuilder.loadingComponent();
this._loadingBuilder = loadingComponentBuilder.withItem(this._divContainer);
}
public component(): azdata.LoadingComponent {
return this._loadingBuilder.component();
}
async load(optionsInfoGetter: () => Promise<RadioOptionsInfo>): Promise<void> {
this.component().loading = true;
this._divContainer.clearItems();
try {
const optionsInfo = await optionsInfoGetter();
const options = optionsInfo.values!;
let defaultValue: string = optionsInfo.defaultValue!;
options.forEach((option: string) => {
const radioOption = this._view!.modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({
label: option,
checked: option === defaultValue,
name: this._groupName,
value: option,
enabled: true
}).component();
if (radioOption.checked) {
this._currentRadioOption = radioOption;
}
this._onNewDisposableCreated(radioOption.onDidClick(() => {
if (this._currentRadioOption !== radioOption) {
// uncheck the previously saved radio option, the ui gets handled correctly even if we did not do this due to the use of the 'groupName',
// however, the checked properties on the radio button do not get updated, so while the stuff works even if we left the previous option checked,
// it is just better to keep things clean.
this._currentRadioOption.checked = false;
this._currentRadioOption = radioOption;
}
}));
this._divContainer.addItem(radioOption);
});
}
catch (e) {
const errorLabel = this._view!.modelBuilder.text().withProperties({ value: getErrorMessage(e), CSSStyles: { 'color': 'Red' } }).component();
this._divContainer.addItem(errorLabel);
}
this.component().loading = false;
}
get value(): string | undefined {
return this._currentRadioOption?.value;
}
}

View File

@@ -0,0 +1,369 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage';
import { convertToGibibyteString } from '../../../common/utils';
import { MiaaModel } from '../../../models/miaaModel';
export class MiaaComputeAndStoragePage extends DashboardPage {
private configurationContainer?: azdata.DivContainer;
private coresLimitBox?: azdata.InputBoxComponent;
private coresRequestBox?: azdata.InputBoxComponent;
private memoryLimitBox?: azdata.InputBoxComponent;
private memoryRequestBox?: azdata.InputBoxComponent;
private discardButton?: azdata.ButtonComponent;
private saveButton?: azdata.ButtonComponent;
private saveArgs: {
coresLimit?: string,
coresRequest?: string,
memoryLimit?: string,
memoryRequest?: string
} = {};
private readonly _azdataApi: azdataExt.IExtension;
constructor(protected modelView: azdata.ModelView, private _miaaModel: MiaaModel) {
super(modelView);
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
this.initializeConfigurationBoxes();
this.disposables.push(this._miaaModel.onConfigUpdated(
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
}
protected get title(): string {
return loc.computeAndStorage;
}
protected get id(): string {
return 'miaa-compute-and-storage';
}
protected get icon(): { dark: string; light: string; } {
return IconPathHelper.computeStorage;
}
protected get container(): azdata.Component {
const root = this.modelView.modelBuilder.divContainer().component();
const content = this.modelView.modelBuilder.divContainer().component();
root.addItem(content, { CSSStyles: { 'margin': '20px' } });
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.computeAndStorage,
CSSStyles: { ...cssStyles.title }
}).component());
const infoComputeStorage_p1 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.miaaComputeAndStorageDescriptionPartOne,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'max-width': 'auto' }
}).component();
const memoryVCoreslink = this.modelView.modelBuilder.hyperlink().withProperties<azdata.HyperlinkComponentProperties>({
label: loc.scalingCompute,
url: 'https://docs.microsoft.com/azure/azure-arc/data/configure-managed-instance',
CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
const infoComputeStorage_p4 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.computeAndStorageDescriptionPartFour,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
const infoComputeStorage_p5 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.computeAndStorageDescriptionPartFive,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
const infoComputeStorage_p6 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.computeAndStorageDescriptionPartSix,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
const computeInfoAndLinks = this.modelView.modelBuilder.flexContainer()
.withLayout({ flexWrap: 'wrap' })
.withItems([
infoComputeStorage_p1,
memoryVCoreslink,
infoComputeStorage_p4,
infoComputeStorage_p5,
infoComputeStorage_p6
], { CSSStyles: { 'margin-right': '5px' } }).component();
content.addItem(computeInfoAndLinks, { CSSStyles: { 'min-height': '30px' } });
this.configurationContainer = this.modelView.modelBuilder.divContainer().component();
this.configurationContainer.addItems(this.createUserInputSection(), { CSSStyles: { 'min-height': '30px' } });
content.addItem(this.configurationContainer, { CSSStyles: { 'margin-top': '30px' } });
this.initialized = true;
return root;
}
protected get toolbarContainer(): azdata.ToolbarContainer {
// Save Edits
this.saveButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
label: loc.saveText,
iconPath: IconPathHelper.save,
enabled: false
}).component();
this.disposables.push(
this.saveButton.onDidClick(async () => {
this.saveButton!.enabled = false;
try {
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.updatingInstance(this._miaaModel.info.name),
cancellable: false
},
async (_progress, _token): Promise<void> => {
try {
await this._azdataApi.azdata.arc.sql.mi.edit(
this._miaaModel.info.name, this.saveArgs);
} catch (err) {
this.saveButton!.enabled = true;
throw err;
}
await this._miaaModel.refresh();
}
);
vscode.window.showInformationMessage(loc.instanceUpdated(this._miaaModel.info.name));
this.discardButton!.enabled = false;
} catch (error) {
vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._miaaModel.info.name, error));
}
}));
// Discard
this.discardButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
label: loc.discardText,
iconPath: IconPathHelper.discard,
enabled: false
}).component();
this.disposables.push(
this.discardButton.onDidClick(async () => {
this.discardButton!.enabled = false;
try {
this.editCores();
this.editMemory();
} catch (error) {
vscode.window.showErrorMessage(loc.pageDiscardFailed(error));
} finally {
this.saveButton!.enabled = false;
}
}));
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
{ component: this.saveButton },
{ component: this.discardButton }
]).component();
}
private initializeConfigurationBoxes() {
this.coresLimitBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 1,
validationErrorMessage: loc.coresValidationErrorMessage,
inputType: 'number',
placeHolder: loc.loading
}).component();
this.disposables.push(
this.coresLimitBox.onTextChanged(() => {
if (!(this.handleOnTextChanged(this.coresLimitBox!))) {
this.saveArgs.coresLimit = undefined;
} else {
this.saveArgs.coresLimit = this.coresLimitBox!.value;
}
})
);
this.coresRequestBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 1,
validationErrorMessage: loc.coresValidationErrorMessage,
inputType: 'number',
placeHolder: loc.loading
}).component();
this.disposables.push(
this.coresRequestBox.onTextChanged(() => {
if (!(this.handleOnTextChanged(this.coresRequestBox!))) {
this.saveArgs.coresRequest = undefined;
} else {
this.saveArgs.coresRequest = this.coresRequestBox!.value;
}
})
);
this.memoryLimitBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 2,
validationErrorMessage: loc.memoryLimitValidationErrorMessage,
inputType: 'number',
placeHolder: loc.loading
}).component();
this.disposables.push(
this.memoryLimitBox.onTextChanged(() => {
if (!(this.handleOnTextChanged(this.memoryLimitBox!))) {
this.saveArgs.memoryLimit = undefined;
} else {
this.saveArgs.memoryLimit = this.memoryLimitBox!.value + 'Gi';
}
})
);
this.memoryRequestBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
readOnly: false,
min: 2,
validationErrorMessage: loc.memoryRequestValidationErrorMessage,
inputType: 'number',
placeHolder: loc.loading
}).component();
this.disposables.push(
this.memoryRequestBox.onTextChanged(() => {
if (!(this.handleOnTextChanged(this.memoryRequestBox!))) {
this.saveArgs.memoryRequest = undefined;
} else {
this.saveArgs.memoryRequest = this.memoryRequestBox!.value + 'Gi';
}
})
);
}
private createUserInputSection(): azdata.Component[] {
if (this._miaaModel.configLastUpdated) {
this.editCores();
this.editMemory();
}
return [
this.createConfigurationSectionContainer(loc.coresRequest, this.coresRequestBox!),
this.createConfigurationSectionContainer(loc.coresLimit, this.coresLimitBox!),
this.createConfigurationSectionContainer(loc.memoryRequest, this.memoryRequestBox!),
this.createConfigurationSectionContainer(loc.memoryLimit, this.memoryLimitBox!)
];
}
private createConfigurationSectionContainer(key: string, input: azdata.Component): azdata.FlexContainer {
const inputFlex = { flex: '0 1 150px' };
const keyFlex = { flex: `0 1 250px` };
const flexContainer = this.modelView.modelBuilder.flexContainer().withLayout({
flexWrap: 'wrap',
alignItems: 'center'
}).component();
const keyComponent = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: key,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
const keyContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component();
keyContainer.addItem(keyComponent, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '15px' } });
flexContainer.addItem(keyContainer, keyFlex);
const inputContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component();
inputContainer.addItem(input, { CSSStyles: { 'margin-bottom': '15px', 'min-width': '50px', 'max-width': '225px' } });
flexContainer.addItem(inputContainer, inputFlex);
return flexContainer;
}
private handleOnTextChanged(component: azdata.InputBoxComponent): boolean {
if ((!component.value)) {
// if there is no text found in the inputbox component return false
return false;
} else if ((!component.valid)) {
// if value given by user is not valid enable discard button for user
// to clear all inputs and return false
this.discardButton!.enabled = true;
return false;
} else {
// if a valid value has been entered into the input box, enable save and discard buttons
// so that user could choose to either edit instance or clear all inputs
// return true
this.saveButton!.enabled = true;
this.discardButton!.enabled = true;
return true;
}
}
private editCores(): void {
let currentCPUSize = this._miaaModel.config?.spec?.requests?.vcores;
if (!currentCPUSize) {
currentCPUSize = '';
}
this.coresRequestBox!.placeHolder = currentCPUSize;
this.coresRequestBox!.value = '';
this.saveArgs.coresRequest = undefined;
currentCPUSize = this._miaaModel.config?.spec?.limits?.vcores;
if (!currentCPUSize) {
currentCPUSize = '';
}
this.coresLimitBox!.placeHolder = currentCPUSize;
this.coresLimitBox!.value = '';
this.saveArgs.coresLimit = undefined;
}
private editMemory(): void {
let currentMemSizeConversion: string;
let currentMemorySize = this._miaaModel.config?.spec?.requests?.memory;
if (!currentMemorySize) {
currentMemSizeConversion = '';
} else {
currentMemSizeConversion = convertToGibibyteString(currentMemorySize);
}
this.memoryRequestBox!.placeHolder = currentMemSizeConversion!;
this.memoryRequestBox!.value = '';
this.saveArgs.memoryRequest = undefined;
currentMemorySize = this._miaaModel.config?.spec?.limits?.memory;
if (!currentMemorySize) {
currentMemSizeConversion = '';
} else {
currentMemSizeConversion = convertToGibibyteString(currentMemorySize);
}
this.memoryLimitBox!.placeHolder = currentMemSizeConversion!;
this.memoryLimitBox!.value = '';
this.saveArgs.memoryLimit = undefined;
}
private handleServiceUpdated() {
this.editCores();
this.editMemory();
}
}

View File

@@ -10,6 +10,7 @@ import { ControllerModel } from '../../../models/controllerModel';
import * as loc from '../../../localizedConstants';
import { MiaaConnectionStringsPage } from './miaaConnectionStringsPage';
import { MiaaModel } from '../../../models/miaaModel';
import { MiaaComputeAndStoragePage } from './miaaComputeAndStoragePage';
export class MiaaDashboard extends Dashboard {
@@ -27,12 +28,14 @@ export class MiaaDashboard extends Dashboard {
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
const overviewPage = new MiaaDashboardOverviewPage(modelView, this._controllerModel, this._miaaModel);
const connectionStringsPage = new MiaaConnectionStringsPage(modelView, this._controllerModel, this._miaaModel);
const computeAndStoragePage = new MiaaComputeAndStoragePage(modelView, this._miaaModel);
return [
overviewPage.tab,
{
title: loc.settings,
tabs: [
connectionStringsPage.tab
connectionStringsPage.tab,
computeAndStoragePage.tab
]
},
];

View File

@@ -67,11 +67,11 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
}).component());
const infoComputeStorage_p1 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.computeAndStorageDescriptionPartOne,
value: loc.postgresComputeAndStorageDescriptionPartOne,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'max-width': 'auto' }
}).component();
const infoComputeStorage_p2 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: loc.computeAndStorageDescriptionPartTwo,
value: loc.postgresComputeAndStorageDescriptionPartTwo,
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
@@ -107,15 +107,19 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
}).component();
const computeInfoAndLinks = this.modelView.modelBuilder.flexContainer().withLayout({ flexWrap: 'wrap' }).component();
computeInfoAndLinks.addItem(infoComputeStorage_p1, { CSSStyles: { 'margin-right': '5px' } });
computeInfoAndLinks.addItem(infoComputeStorage_p2, { CSSStyles: { 'margin-right': '5px' } });
computeInfoAndLinks.addItem(workerNodeslink, { CSSStyles: { 'margin-right': '5px' } });
computeInfoAndLinks.addItem(infoComputeStorage_p3, { CSSStyles: { 'margin-right': '5px' } });
computeInfoAndLinks.addItem(memoryVCoreslink, { CSSStyles: { 'margin-right': '5px' } });
computeInfoAndLinks.addItem(infoComputeStorage_p4, { CSSStyles: { 'margin-right': '5px' } });
computeInfoAndLinks.addItem(infoComputeStorage_p5, { CSSStyles: { 'margin-right': '5px' } });
computeInfoAndLinks.addItem(infoComputeStorage_p6, { CSSStyles: { 'margin-right': '5px' } });
const computeInfoAndLinks = this.modelView.modelBuilder.flexContainer()
.withLayout({ flexWrap: 'wrap' })
.withItems([
infoComputeStorage_p1,
infoComputeStorage_p2,
workerNodeslink,
infoComputeStorage_p3,
memoryVCoreslink,
infoComputeStorage_p4,
infoComputeStorage_p5,
infoComputeStorage_p6
], { CSSStyles: { 'margin-right': '5px' } })
.component();
content.addItem(computeInfoAndLinks, { CSSStyles: { 'min-height': '30px' } });
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
@@ -151,8 +155,15 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
cancellable: false
},
async (_progress, _token): Promise<void> => {
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name, this.saveArgs);
try {
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name, this.saveArgs);
} catch (err) {
// If an error occurs while editing the instance then re-enable the save button since
// the edit wasn't successfully applied
this.saveButton!.enabled = true;
throw err;
}
await this._postgresModel.refresh();
}
);
@@ -415,7 +426,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
const information = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
iconPath: IconPathHelper.information,
title: loc.configurationInformation,
title: loc.postgresConfigurationInformation,
width: '12px',
height: '12px',
enabled: false

View File

@@ -270,6 +270,11 @@
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f"
integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==
"@types/yamljs@^0.2.31":
version "0.2.31"
resolved "https://registry.yarnpkg.com/@types/yamljs/-/yamljs-0.2.31.tgz#b1a620b115c96db7b3bfdf0cf54aee0c57139245"
integrity sha512-QcJ5ZczaXAqbVD3o8mw/mEBhRvO5UAdTtbvgwL/OgoWubvNBh6/MxLBAigtcgIFaq3shon9m3POIxQaLQt4fxQ==
ajv@^6.5.5:
version "6.12.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7"
@@ -299,6 +304,13 @@ append-transform@^2.0.0:
dependencies:
default-require-extensions "^3.0.0"
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
dependencies:
sprintf-js "~1.0.2"
asn1@~0.2.3:
version "0.2.4"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
@@ -580,7 +592,7 @@ glob@7.1.2:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.1.2, glob@^7.1.3:
glob@^7.0.5, glob@^7.1.2, glob@^7.1.3:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -1109,6 +1121,11 @@ source-map@^0.6.1:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
sshpk@^1.7.0:
version "1.16.1"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
@@ -1251,3 +1268,11 @@ xml@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"
integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=
yamljs@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/yamljs/-/yamljs-0.3.0.tgz#dc060bf267447b39f7304e9b2bfbe8b5a7ddb03b"
integrity sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==
dependencies:
argparse "^1.0.7"
glob "^7.0.5"

View File

@@ -53,13 +53,7 @@
{
"cell_type": "code",
"source": [
"import pandas,sys,os,json,html,getpass,time,ntpath,uuid\n",
"pandas_version = pandas.__version__.split('.')\n",
"pandas_major = int(pandas_version[0])\n",
"pandas_minor = int(pandas_version[1])\n",
"pandas_patch = int(pandas_version[2])\n",
"if not (pandas_major > 0 or (pandas_major == 0 and pandas_minor > 24) or (pandas_major == 0 and pandas_minor == 24 and pandas_patch >= 2)):\n",
" sys.exit('Please upgrade the Notebook dependency before you can proceed, you can do it by running the \"Reinstall Notebook dependencies\" command in command palette (View menu -> Command Palette…).')\n",
"import sys,os,json,html,getpass,time,ntpath,uuid\n",
"\n",
"def run_command(command:str, displayCommand:str = \"\", returnObject:bool = False):\n",
" print(\"Executing: \" + (displayCommand if displayCommand != \"\" else command))\n",

View File

@@ -48,14 +48,8 @@
{
"cell_type": "code",
"source": [
"import pandas,sys,os,getpass,json,html,time\n",
"import sys,os,getpass,json,html,time\n",
"from string import Template\n",
"pandas_version = pandas.__version__.split('.')\n",
"pandas_major = int(pandas_version[0])\n",
"pandas_minor = int(pandas_version[1])\n",
"pandas_patch = int(pandas_version[2])\n",
"if not (pandas_major > 0 or (pandas_major == 0 and pandas_minor > 24) or (pandas_major == 0 and pandas_minor == 24 and pandas_patch >= 2)):\n",
" sys.exit('Please upgrade the Notebook dependency before you can proceed, you can do it by running the \"Reinstall Notebook dependencies\" command in command palette (View menu -> Command Palette…).')\n",
"\n",
"def run_command(displayCommand = \"\"):\n",
" print(\"Executing: \" + (displayCommand if displayCommand != \"\" else cmd))\n",

View File

@@ -48,13 +48,7 @@
{
"cell_type": "code",
"source": [
"import pandas,sys,os,json,html,getpass,time,ntpath,uuid\n",
"pandas_version = pandas.__version__.split('.')\n",
"pandas_major = int(pandas_version[0])\n",
"pandas_minor = int(pandas_version[1])\n",
"pandas_patch = int(pandas_version[2])\n",
"if not (pandas_major > 0 or (pandas_major == 0 and pandas_minor > 24) or (pandas_major == 0 and pandas_minor == 24 and pandas_patch >= 2)):\n",
" sys.exit('Please upgrade the Notebook dependency before you can proceed, you can do it by running the \"Reinstall Notebook dependencies\" command in command palette (View menu -> Command Palette…).')\n",
"import sys,os,json,html,getpass,time,ntpath,uuid\n",
"\n",
"def run_command(command:str, displayCommand:str = \"\", returnObject:bool = False):\n",
" print(\"Executing: \" + (displayCommand if displayCommand != \"\" else command))\n",

View File

@@ -46,13 +46,7 @@
{
"cell_type": "code",
"source": [
"import pandas,sys,getpass,os,json,html,time\n",
"pandas_version = pandas.__version__.split('.')\n",
"pandas_major = int(pandas_version[0])\n",
"pandas_minor = int(pandas_version[1])\n",
"pandas_patch = int(pandas_version[2])\n",
"if not (pandas_major > 0 or (pandas_major == 0 and pandas_minor > 24) or (pandas_major == 0 and pandas_minor == 24 and pandas_patch >= 2)):\n",
" sys.exit('Please upgrade the Notebook dependency before you can proceed, you can do it by running the \"Reinstall Notebook dependencies\" command in command palette (View menu -> Command Palette…).')\n",
"import sys,getpass,os,json,html,time\n",
"\n",
"def run_command():\n",
" print(\"Executing: \" + cmd)\n",

View File

@@ -51,13 +51,7 @@
{
"cell_type": "code",
"source": [
"import pandas,sys,os,json,html,getpass,time,ntpath,uuid\n",
"pandas_version = pandas.__version__.split('.')\n",
"pandas_major = int(pandas_version[0])\n",
"pandas_minor = int(pandas_version[1])\n",
"pandas_patch = int(pandas_version[2])\n",
"if not (pandas_major > 0 or (pandas_major == 0 and pandas_minor > 24) or (pandas_major == 0 and pandas_minor == 24 and pandas_patch >= 2)):\n",
" sys.exit('Please upgrade the Notebook dependency before you can proceed, you can do it by running the \"Reinstall Notebook dependencies\" command in command palette (View menu -> Command Palette…).')\n",
"import sys,os,json,html,getpass,time,ntpath,uuid\n",
"\n",
"def run_command(command:str, displayCommand:str = \"\", returnObject:bool = False):\n",
" print(\"Executing: \" + (displayCommand if displayCommand != \"\" else command))\n",

View File

@@ -296,7 +296,6 @@
"defaultValue": "westus",
"required": true,
"locationVariableName": "AZDATA_NB_VAR_ASDE_AZURE_LOCATION",
"displayLocationVariableName": "AZDATA_NB_VAR_ASDE_AZURE_LOCATION_TEXT",
"locations": [
"australiaeast",
"australiasoutheast",
@@ -339,9 +338,11 @@
"confirmationRequired": true,
"confirmationLabel": "%vm_password_confirm%",
"required": true,
"textValidationRequired": true,
"textValidationRegex": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[\\W_])[A-Za-z\\d\\W_]{12,123}$",
"textValidationDescription": "%vm_password_validation_error_message%"
"validations" : [{
"type": "regex_match",
"regex": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[\\W_])[A-Za-z\\d\\W_]{12,123}$",
"description": "%vm_password_validation_error_message%"
}]
}
]
},

View File

@@ -125,6 +125,19 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.show(name);
},
edit: async (
name: string,
args: {
coresLimit?: string;
coresRequest?: string;
memoryLimit?: string;
memoryRequest?: string;
noWait?: boolean;
}) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.edit(name, args);
}
}
}

View File

@@ -121,16 +121,16 @@ export class AzdataTool implements azdataExt.IAzdataApi {
additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<void>> => {
const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name];
if (args.adminPassword) { argsArray.push('--admin-password'); }
if (args.coresLimit !== undefined) { argsArray.push('--cores-limit', args.coresLimit); }
if (args.coresRequest !== undefined) { argsArray.push('--cores-request', args.coresRequest); }
if (args.engineSettings !== undefined) { argsArray.push('--engine-settings', args.engineSettings); }
if (args.extensions !== undefined) { argsArray.push('--extensions', args.extensions); }
if (args.memoryLimit !== undefined) { argsArray.push('--memory-limit', args.memoryLimit); }
if (args.memoryRequest !== undefined) { argsArray.push('--memory-request', args.memoryRequest); }
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
if (args.coresRequest) { argsArray.push('--cores-request', args.coresRequest); }
if (args.engineSettings) { argsArray.push('--engine-settings', args.engineSettings); }
if (args.extensions) { argsArray.push('--extensions', args.extensions); }
if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); }
if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
if (args.noWait) { argsArray.push('--no-wait'); }
if (args.port !== undefined) { argsArray.push('--port', args.port.toString()); }
if (args.port) { argsArray.push('--port', args.port.toString()); }
if (args.replaceEngineSettings) { argsArray.push('--replace-engine-settings'); }
if (args.workers !== undefined) { argsArray.push('--workers', args.workers.toString()); }
if (args.workers) { argsArray.push('--workers', args.workers.toString()); }
return this.executeCommand<void>(argsArray, additionalEnvVars);
}
}
@@ -145,6 +145,23 @@ export class AzdataTool implements azdataExt.IAzdataApi {
},
show: (name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> => {
return this.executeCommand<azdataExt.SqlMiShowResult>(['arc', 'sql', 'mi', 'show', '-n', name]);
},
edit: (
name: string,
args: {
coresLimit?: string,
coresRequest?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
}): Promise<azdataExt.AzdataOutput<void>> => {
const argsArray = ['arc', 'sql', 'mi', 'edit', '-n', name];
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
if (args.coresRequest) { argsArray.push('--cores-request', args.coresRequest); }
if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); }
if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
if (args.noWait) { argsArray.push('--no-wait'); }
return this.executeCommand<void>(argsArray);
}
}
}

View File

@@ -125,7 +125,12 @@ declare module 'azdata-ext' {
},
spec: {
limits?: {
vcores?: number // 4
memory?: string // "10Gi"
vcores?: string // "4"
},
requests?: {
memory?: string // "10Gi"
vcores?: string // "4"
}
service: {
type: string // "NodePort"
@@ -264,7 +269,17 @@ declare module 'azdata-ext' {
mi: {
delete(name: string): Promise<AzdataOutput<void>>,
list(): Promise<AzdataOutput<SqlMiListResult[]>>,
show(name: string): Promise<AzdataOutput<SqlMiShowResult>>
show(name: string): Promise<AzdataOutput<SqlMiShowResult>>,
edit(
name: string,
args: {
coresLimit?: string,
coresRequest?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
}
): Promise<AzdataOutput<void>>
}
}
},

View File

@@ -141,7 +141,12 @@
"icon": "$(refresh)"
},
{
"command": "azure.resource.refresh",
"command": "azure.resource.azureview.refresh",
"title": "%azure.resource.refresh.title%",
"icon": "$(refresh)"
},
{
"command": "azure.resource.connectiondialog.refresh",
"title": "%azure.resource.refresh.title%",
"icon": "$(refresh)"
},
@@ -209,7 +214,11 @@
"when": "false"
},
{
"command": "azure.resource.refresh",
"command": "azure.resource.azureview.refresh",
"when": "false"
},
{
"command": "azure.resource.connectiondialog.refresh",
"when": "false"
},
{
@@ -245,12 +254,12 @@
"group": "azurecore"
},
{
"command": "azure.resource.refresh",
"command": "azure.resource.azureview.refresh",
"when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:account|subscription|databaseContainer|databaseServerContainer)$/",
"group": "inline"
},
{
"command": "azure.resource.refresh",
"command": "azure.resource.azureview.refresh",
"when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:account|subscription|databaseContainer|databaseServerContainer)$/",
"group": "azurecore"
},
@@ -287,7 +296,7 @@
"group": "navigation"
},
{
"command": "azure.resource.refresh",
"command": "azure.resource.connectiondialog.refresh",
"when": "contextValue == azure.resource.itemType.account",
"group": "navigation"
},

View File

@@ -21,7 +21,8 @@ import { AzureAccount, Tenant } from '../account-provider/interfaces';
import { FlatAccountTreeNode } from './tree/flatAccountTreeNode';
import { ConnectionDialogTreeProvider } from './tree/connectionDialogTreeProvider';
export function registerAzureResourceCommands(appContext: AppContext, trees: (AzureResourceTreeProvider | ConnectionDialogTreeProvider)[]): void {
export function registerAzureResourceCommands(appContext: AppContext, azureViewTree: AzureResourceTreeProvider, connectionDialogTree: ConnectionDialogTreeProvider): void {
const trees = [azureViewTree, connectionDialogTree];
vscode.commands.registerCommand('azure.resource.startterminal', async (node?: TreeNode) => {
try {
const enablePreviewFeatures = vscode.workspace.getConfiguration('workbench').get('enablePreviewFeatures');
@@ -168,10 +169,12 @@ export function registerAzureResourceCommands(appContext: AppContext, trees: (Az
}
});
vscode.commands.registerCommand('azure.resource.refresh', async (node?: TreeNode) => {
for (const tree of trees) {
await tree.refresh(node, true);
}
vscode.commands.registerCommand('azure.resource.azureview.refresh', async (node?: TreeNode) => {
await azureViewTree.refresh(node, true);
});
vscode.commands.registerCommand('azure.resource.connectiondialog.refresh', async (node?: TreeNode) => {
await connectionDialogTree.refresh(node, true);
});
vscode.commands.registerCommand('azure.resource.signin', async (node?: TreeNode) => {

View File

@@ -18,7 +18,7 @@ import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
import { AzureResourceItemType, AzureResourceServiceNames } from '../constants';
import { AzureResourceMessageTreeNode } from '../messageTreeNode';
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureResourceNodeWithProviderId } from '../../azureResource/interfaces';
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService } from '../../azureResource/interfaces';
import { AzureAccount } from '../../account-provider/interfaces';
import { AzureResourceService } from '../resourceService';
import { AzureResourceResourceTreeNode } from '../resourceTreeNode';
@@ -39,11 +39,22 @@ export class FlatAccountTreeNode extends AzureResourceContainerTreeNodeBase {
this._id = `account_${this.account.key.accountId}`;
this.setCacheKey(`${this._id}.dataresources`);
this._label = account.displayInfo.displayName;
this._loader = new FlatAccountTreeNodeLoader(appContext, this._resourceService, this._subscriptionService, this._subscriptionFilterService, this.account, this);
this._loader.onNewResourcesAvailable(() => {
this.treeChangeHandler.notifyNodeChanged(this);
});
this._loader.onLoadingStatusChanged(async () => {
await this.updateLabel();
this.treeChangeHandler.notifyNodeChanged(this);
});
}
public async updateLabel(): Promise<void> {
const subscriptionInfo = await this.getSubscriptionInfo();
if (subscriptionInfo.total !== 0) {
const subscriptionInfo = await getSubscriptionInfo(this.account, this._subscriptionService, this._subscriptionFilterService);
if (this._loader.isLoading) {
this._label = localize('azure.resource.tree.accountTreeNode.titleLoading', "{0} - Loading...", this.account.displayInfo.displayName);
} else if (subscriptionInfo.total !== 0) {
this._label = localize({
key: 'azure.resource.tree.accountTreeNode.title',
comment: [
@@ -57,79 +68,13 @@ export class FlatAccountTreeNode extends AzureResourceContainerTreeNodeBase {
}
}
private async getSubscriptionInfo(): Promise<{
subscriptions: azureResource.AzureResourceSubscription[],
total: number,
selected: number
}> {
let subscriptions: azureResource.AzureResourceSubscription[] = [];
try {
for (const tenant of this.account.properties.tenants) {
const token = await azdata.accounts.getAccountSecurityToken(this.account, tenant.id, azdata.AzureResource.ResourceManagement);
subscriptions.push(...(await this._subscriptionService.getSubscriptions(this.account, new TokenCredentials(token.token, token.tokenType), tenant.id) || <azureResource.AzureResourceSubscription[]>[]));
}
} catch (error) {
throw new AzureResourceCredentialError(localize('azure.resource.tree.accountTreeNode.credentialError', "Failed to get credential for account {0}. Please refresh the account.", this.account.key.accountId), error);
}
const total = subscriptions.length;
let selected = total;
const selectedSubscriptions = await this._subscriptionFilterService.getSelectedSubscriptions(this.account);
const selectedSubscriptionIds = (selectedSubscriptions || <azureResource.AzureResourceSubscription[]>[]).map((subscription) => subscription.id);
if (selectedSubscriptionIds.length > 0) {
subscriptions = subscriptions.filter((subscription) => selectedSubscriptionIds.indexOf(subscription.id) !== -1);
selected = selectedSubscriptionIds.length;
}
return {
subscriptions,
total,
selected
};
}
public async getChildren(): Promise<TreeNode[]> {
try {
let dataResources: IAzureResourceNodeWithProviderId[] = [];
if (this._isClearingCache) {
let subscriptions: azureResource.AzureResourceSubscription[] = (await this.getSubscriptionInfo()).subscriptions;
if (subscriptions.length === 0) {
return [AzureResourceMessageTreeNode.create(FlatAccountTreeNode.noSubscriptionsLabel, this)];
} else {
// Filter out everything that we can't authenticate to.
subscriptions = subscriptions.filter(async s => {
const token = await azdata.accounts.getAccountSecurityToken(this.account, s.tenant, azdata.AzureResource.ResourceManagement);
if (!token) {
console.info(`Account does not have permissions to view subscription ${JSON.stringify(s)}.`);
return false;
}
return true;
});
}
const resourceProviderIds = await this._resourceService.listResourceProviderIds();
for (const subscription of subscriptions) {
for (const providerId of resourceProviderIds) {
const resourceTypes = await this._resourceService.getRootChildren(providerId, this.account, subscription, subscription.tenant);
for (const resourceType of resourceTypes) {
dataResources.push(...await this._resourceService.getChildren(providerId, resourceType.resourceNode, true));
}
}
}
dataResources = dataResources.sort((a, b) => { return a.resourceNode.treeItem.label.localeCompare(b.resourceNode.treeItem.label); });
this.updateCache(dataResources);
this._isClearingCache = false;
} else {
dataResources = this.getCache<IAzureResourceNodeWithProviderId[]>();
}
return dataResources.map(dr => new AzureResourceResourceTreeNode(dr, this, this.appContext));
} catch (error) {
if (error instanceof AzureResourceCredentialError) {
vscode.commands.executeCommand('azure.resource.signin');
}
return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)];
if (this._isClearingCache) {
this._loader.start();
this._isClearingCache = false;
return [];
} else {
return this._loader.nodes;
}
}
@@ -162,12 +107,126 @@ export class FlatAccountTreeNode extends AzureResourceContainerTreeNodeBase {
return this._id;
}
private _subscriptionService: IAzureResourceSubscriptionService = undefined;
private _subscriptionFilterService: IAzureResourceSubscriptionFilterService = undefined;
private _resourceService: AzureResourceService = undefined;
private _id: string = undefined;
private _label: string = undefined;
private static readonly noSubscriptionsLabel = localize('azure.resource.tree.accountTreeNode.noSubscriptionsLabel', "No Subscriptions found.");
private _subscriptionService: IAzureResourceSubscriptionService;
private _subscriptionFilterService: IAzureResourceSubscriptionFilterService;
private _resourceService: AzureResourceService;
private _loader: FlatAccountTreeNodeLoader;
private _id: string;
private _label: string;
}
async function getSubscriptionInfo(account: AzureAccount, subscriptionService: IAzureResourceSubscriptionService, subscriptionFilterService: IAzureResourceSubscriptionFilterService): Promise<{
subscriptions: azureResource.AzureResourceSubscription[],
total: number,
selected: number
}> {
let subscriptions: azureResource.AzureResourceSubscription[] = [];
try {
for (const tenant of account.properties.tenants) {
const token = await azdata.accounts.getAccountSecurityToken(account, tenant.id, azdata.AzureResource.ResourceManagement);
subscriptions.push(...(await subscriptionService.getSubscriptions(account, new TokenCredentials(token.token, token.tokenType), tenant.id) || <azureResource.AzureResourceSubscription[]>[]));
}
} catch (error) {
throw new AzureResourceCredentialError(localize('azure.resource.tree.accountTreeNode.credentialError', "Failed to get credential for account {0}. Please refresh the account.", account.key.accountId), error);
}
const total = subscriptions.length;
let selected = total;
const selectedSubscriptions = await subscriptionFilterService.getSelectedSubscriptions(account);
const selectedSubscriptionIds = (selectedSubscriptions || <azureResource.AzureResourceSubscription[]>[]).map((subscription) => subscription.id);
if (selectedSubscriptionIds.length > 0) {
subscriptions = subscriptions.filter((subscription) => selectedSubscriptionIds.indexOf(subscription.id) !== -1);
selected = selectedSubscriptionIds.length;
}
return {
subscriptions,
total,
selected
};
}
class FlatAccountTreeNodeLoader {
private _isLoading: boolean = false;
private _nodes: TreeNode[];
private readonly _onNewResourcesAvailable = new vscode.EventEmitter<void>();
public readonly onNewResourcesAvailable = this._onNewResourcesAvailable.event;
private readonly _onLoadingStatusChanged = new vscode.EventEmitter<void>();
public readonly onLoadingStatusChanged = this._onLoadingStatusChanged.event;
constructor(private readonly appContext: AppContext,
private readonly _resourceService: AzureResourceService,
private readonly _subscriptionService: IAzureResourceSubscriptionService,
private readonly _subscriptionFilterService: IAzureResourceSubscriptionFilterService,
private readonly _account: AzureAccount,
private readonly _accountNode: TreeNode) {
}
public get isLoading(): boolean {
return this._isLoading;
}
public get nodes(): TreeNode[] {
return this._nodes;
}
public async start(): Promise<void> {
if (this._isLoading) {
return;
}
this._isLoading = true;
this._nodes = [];
this._onLoadingStatusChanged.fire();
let newNodesAvailable = false;
// Throttle the refresh events to at most once per 500ms
const refreshHandle = setInterval(() => {
if (newNodesAvailable) {
this._onNewResourcesAvailable.fire();
newNodesAvailable = false;
}
if (!this.isLoading) {
clearInterval(refreshHandle);
}
}, 500);
try {
let subscriptions: azureResource.AzureResourceSubscription[] = (await getSubscriptionInfo(this._account, this._subscriptionService, this._subscriptionFilterService)).subscriptions;
if (subscriptions.length !== 0) {
// Filter out everything that we can't authenticate to.
subscriptions = subscriptions.filter(async s => {
const token = await azdata.accounts.getAccountSecurityToken(this._account, s.tenant, azdata.AzureResource.ResourceManagement);
if (!token) {
console.info(`Account does not have permissions to view subscription ${JSON.stringify(s)}.`);
return false;
}
return true;
});
}
const resourceProviderIds = await this._resourceService.listResourceProviderIds();
for (const subscription of subscriptions) {
for (const providerId of resourceProviderIds) {
const resourceTypes = await this._resourceService.getRootChildren(providerId, this._account, subscription, subscription.tenant);
for (const resourceType of resourceTypes) {
const resources = await this._resourceService.getChildren(providerId, resourceType.resourceNode, true);
if (resources.length > 0) {
this._nodes.push(...resources.map(dr => new AzureResourceResourceTreeNode(dr, this._accountNode, this.appContext)));
this._nodes = this.nodes.sort((a, b) => {
return a.getNodeInfo().label.localeCompare(b.getNodeInfo().label);
});
newNodesAvailable = true;
}
}
}
}
} catch (error) {
if (error instanceof AzureResourceCredentialError) {
vscode.commands.executeCommand('azure.resource.signin');
}
this._nodes = [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this._accountNode)];
}
this._isLoading = false;
this._onLoadingStatusChanged.fire();
}
}

View File

@@ -89,7 +89,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
pushDisposable(vscode.window.registerTreeDataProvider('connectionDialog/azureResourceExplorer', connectionDialogTree));
pushDisposable(vscode.window.registerTreeDataProvider('azureResourceExplorer', azureResourceTree));
pushDisposable(vscode.workspace.onDidChangeConfiguration(e => onDidChangeConfiguration(e), this));
registerAzureResourceCommands(appContext, [azureResourceTree, connectionDialogTree]);
registerAzureResourceCommands(appContext, azureResourceTree, connectionDialogTree);
azdata.dataprotocol.registerDataGridProvider(new AzureDataGridProvider(appContext));
vscode.commands.registerCommand('azure.dataGrid.openInAzurePortal', async (item: azdata.DataGridItem) => {
const portalEndpoint = item.portalEndpoint;

View File

@@ -0,0 +1,2 @@
notebooks/hybridbook/Components/**/obj
*.vsix

View File

@@ -1,4 +1,7 @@
.gitignore
src/**
out/**
tsconfig.json
extension.webpack.config.js
*.vsix
yarn.lock

View File

@@ -1,6 +1,6 @@
# Azure SQL Hybrid Cloud Toolkit Jupyter Book Extension for Azure Data Studio
# Azure SQL Hybrid Cloud Toolkit *(preview)*
Welcome to the Azure SQL Hybrid Cloud Toolkit Jupyter Book Extension for Azure Data Studio! This extension opens a Jupyter Book that has several utilities for Azure SQL such as migration assessments and setting up networking connectivity.
Adds a Jupyter Book that has several utilities for Azure SQL Hybrid Cloud.
## Code of Conduct

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -8,6 +8,15 @@
- title: Search
search: true
- title: Assessment
url: /Assessments/readme
not_numbered: true
expand_sections: false
sections:
- title: SQL Server Assessment Tool
url: Assessments/sql-server-assessment
- title: Compatibility Assessment
url: Assessments/compatibility-assessment
- title: Networking
url: /networking/readme
not_numbered: true
@@ -19,15 +28,6 @@
url: networking/p2svnet-creation
- title: Create Site-to-Site VPN
url: networking/s2svnet-creation
- title: Assessments
url: /Assessments/readme
not_numbered: true
expand_sections: false
sections:
- title: SQL Server Best Practices Assessment
url: Assessments/sql-server-assessment
- title: Compatibility Assessment
url: Assessments/compatibility-assessment
- title: Provisioning
url: /provisioning/readme
not_numbered: true
@@ -57,8 +57,6 @@
sections:
- title: Backup Database to Blob Storage
url: hadr/backup-to-blob
- title: Add Azure Passive Secondary Replica
url: hadr/add-passive-secondary
- title: Offline Migration
url: /offline-migration/readme
not_numbered: true
@@ -68,13 +66,9 @@
url: offline-migration/instance-to-VM
- title: Migrate Database to Azure SQL VM
url: offline-migration/db-to-VM
- title: Migrate Instance to Azure SQL MI
url: offline-migration/instance-to-MI
- title: Migrate Database to Azure SQL MI
url: offline-migration/db-to-MI
- title: Migrate Database to Azure SQL DB
url: offline-migration/db-to-SQLDB
- title: Glossary
url: /glossary
- title: Appendices
url: /appendices
url: /appendices

View File

@@ -1,4 +0,0 @@
---
---
@import 'main';

View File

@@ -1,4 +0,0 @@
/* Put your custom CSS here */
.left {
margin-left: 0px;
}

View File

@@ -1 +0,0 @@
// Put your custom javascript here

View File

@@ -1,25 +0,0 @@
---
permalink: /index.html
title: "Index"
layout: none
---
<!-- The index page should simply re-direct to the first chapter -->
{% for chapter in site.data.toc %}
{% unless chapter.external %}
{% comment %}This ensures that the first link we re-direct to isn't an external site {% endcomment %}
{% assign redirectURL = chapter.url | relative_url %}
{% break %}
{% endunless %}
{% endfor %}
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>Redirecting&hellip;</title>
<link rel="canonical" href="{{ redirectURL }}">
<script>location="{{ redirectURL }}"</script>
<meta http-equiv="refresh" content="0; url={{ redirectURL }}">
<meta name="robots" content="noindex">
<h1>Redirecting&hellip;</h1>
<a href="{{ redirectURL }}">Click here if you are not redirected.</a>
</html>

View File

@@ -1,15 +0,0 @@
---
permalink: /search
title: "Search the site"
search_page: true
---
<div class="search-content__inner-wrap">
<input type="text" id="lunr_search" class="search-input" tabindex="-1" placeholder="'Enter your search term...''" />
<div id="results" class="results"></div>
</div>
<script>
// Add the lunr store since we will now search it
{% include search/lunr/lunr-store.js %}
</script>

View File

@@ -1 +0,0 @@
<svg aria-hidden="true" data-prefix="far" data-icon="copy" class="svg-inline--fa fa-copy fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="#777" d="M433.941 65.941l-51.882-51.882A48 48 0 0 0 348.118 0H176c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h224c26.51 0 48-21.49 48-48v-48h80c26.51 0 48-21.49 48-48V99.882a48 48 0 0 0-14.059-33.941zM266 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h74v224c0 26.51 21.49 48 48 48h96v42a6 6 0 0 1-6 6zm128-96H182a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h106v88c0 13.255 10.745 24 24 24h88v202a6 6 0 0 1-6 6zm6-256h-64V48h9.632c1.591 0 3.117.632 4.243 1.757l48.368 48.368a6 6 0 0 1 1.757 4.243V112z"></path></svg>

Before

Width:  |  Height:  |  Size: 711 B

View File

@@ -1,81 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg2157"
height="600"
width="600"
version="1.0"
sodipodi:docname="edit-button.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<defs
id="defs12" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1001"
id="namedview10"
showgrid="false"
inkscape:zoom="0.39333333"
inkscape:cx="300"
inkscape:cy="300"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<metadata
id="metadata2162">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
style="fill:#ffffff">
<g
id="g3765"
stroke="#a2a9b1"
fill="none"
style="fill:none;stroke:#6c7681;stroke-opacity:1;stroke-linecap:round;paint-order:normal;stroke-linejoin:miter;stroke-width:40;stroke-miterlimit:5;stroke-dasharray:none">
<path
id="rect2990"
d="m70.064 422.35 374.27-374.26 107.58 107.58-374.26 374.27-129.56 21.97z"
stroke-width="30"
style="fill:none;stroke:#6c7681;stroke-opacity:1;stroke-linecap:round;paint-order:normal;stroke-linejoin:miter;stroke-width:40;stroke-miterlimit:5;stroke-dasharray:none" />
<path
id="path3771"
d="m70.569 417.81 110.61 110.61"
stroke-width="25"
style="fill:none;stroke:#6c7681;stroke-opacity:1;stroke-linecap:round;paint-order:normal;stroke-linejoin:miter;stroke-width:40;stroke-miterlimit:5;stroke-dasharray:none" />
<path
id="path3777"
d="m491.47 108.37-366.69 366.68"
stroke-width="25"
style="fill:none;stroke:#6c7681;stroke-opacity:1;stroke-linecap:round;paint-order:normal;stroke-linejoin:miter;stroke-width:40;stroke-miterlimit:5;stroke-dasharray:none" />
<path
id="path3763"
d="m54.222 507.26 40.975 39.546"
stroke-width="25"
style="fill:none;stroke:#6c7681;stroke-opacity:1;stroke-linecap:round;paint-order:normal;stroke-linejoin:miter;stroke-width:40;stroke-miterlimit:5;stroke-dasharray:none" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 44.4 44.4" style="enable-background:new 0 0 44.4 44.4;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#F5A252;stroke-width:5;stroke-miterlimit:10;}
.st1{fill:none;stroke:#579ACA;stroke-width:5;stroke-miterlimit:10;}
.st2{fill:none;stroke:#E66581;stroke-width:5;stroke-miterlimit:10;}
</style>
<title>logo</title>
<g>
<path class="st0" d="M33.9,6.4c3.6,3.9,3.4,9.9-0.5,13.5s-9.9,3.4-13.5-0.5s-3.4-9.9,0.5-13.5l0,0C24.2,2.4,30.2,2.6,33.9,6.4z"/>
<path class="st1" d="M35.1,27.3c2.6,4.6,1.1,10.4-3.5,13c-4.6,2.6-10.4,1.1-13-3.5s-1.1-10.4,3.5-13l0,0
C26.6,21.2,32.4,22.7,35.1,27.3z"/>
<path class="st2" d="M25.9,17.8c2.6,4.6,1.1,10.4-3.5,13s-10.4,1.1-13-3.5s-1.1-10.4,3.5-13l0,0C17.5,11.7,23.3,13.2,25.9,17.8z"/>
<path class="st1" d="M19.2,26.4c3.1-4.3,9.1-5.2,13.3-2.1c1.1,0.8,2,1.8,2.7,3"/>
<path class="st0" d="M19.9,19.4c-3.6-3.9-3.4-9.9,0.5-13.5s9.9-3.4,13.5,0.5"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="38.73" height="50" viewBox="0 0 38.73 50"><defs><style>.cls-1{fill:#767677;}.cls-2{fill:#f37726;}.cls-3{fill:#9e9e9e;}.cls-4{fill:#616262;}.cls-5{font-size:17.07px;fill:#fff;font-family:Roboto-Regular, Roboto;}</style></defs><title>logo_jupyterhub</title><g id="Canvas"><path id="path7_fill" data-name="path7 fill" class="cls-1" d="M39.51,3.53a3,3,0,0,1-1.7,2.9A3,3,0,0,1,34.48,6a3,3,0,0,1-.82-3.26,3,3,0,0,1,1.05-1.41A3,3,0,0,1,37.52.86a2.88,2.88,0,0,1,1,.6,3,3,0,0,1,.7.93,3.18,3.18,0,0,1,.28,1.14Z" transform="translate(-1.87 -0.69)"/><path id="path8_fill" data-name="path8 fill" class="cls-2" d="M21.91,38.39c-8,0-15.06-2.87-18.7-7.12a19.93,19.93,0,0,0,37.39,0C37,35.52,30,38.39,21.91,38.39Z" transform="translate(-1.87 -0.69)"/><path id="path9_fill" data-name="path9 fill" class="cls-2" d="M21.91,10.78c8,0,15.05,2.87,18.69,7.12a19.93,19.93,0,0,0-37.39,0C6.85,13.64,13.86,10.78,21.91,10.78Z" transform="translate(-1.87 -0.69)"/><path id="path10_fill" data-name="path10 fill" class="cls-3" d="M10.88,46.66a3.86,3.86,0,0,1-.52,2.15,3.81,3.81,0,0,1-1.62,1.51,3.93,3.93,0,0,1-2.19.34,3.79,3.79,0,0,1-2-.94,3.73,3.73,0,0,1-1.14-1.9,3.79,3.79,0,0,1,.1-2.21,3.86,3.86,0,0,1,1.33-1.78,3.92,3.92,0,0,1,3.54-.53,3.85,3.85,0,0,1,2.14,1.93,3.74,3.74,0,0,1,.37,1.43Z" transform="translate(-1.87 -0.69)"/><path id="path11_fill" data-name="path11 fill" class="cls-4" d="M4.12,9.81A2.18,2.18,0,0,1,2.9,9.48a2.23,2.23,0,0,1-.84-1A2.26,2.26,0,0,1,1.9,7.26a2.13,2.13,0,0,1,.56-1.13,2.18,2.18,0,0,1,2.36-.56,2.13,2.13,0,0,1,1,.76,2.18,2.18,0,0,1,.42,1.2A2.22,2.22,0,0,1,4.12,9.81Z" transform="translate(-1.87 -0.69)"/></g><text class="cls-5" transform="translate(5.24 30.01)">Hub</text></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,58 +0,0 @@
(function(){var $c=function(a){this.w=a||[]};$c.prototype.set=function(a){this.w[a]=!0};$c.prototype.encode=function(){for(var a=[],b=0;b<this.w.length;b++)this.w[b]&&(a[Math.floor(b/6)]^=1<<b%6);for(b=0;b<a.length;b++)a[b]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".charAt(a[b]||0);return a.join("")+"~"};var vd=new $c;function J(a){vd.set(a)}var Td=function(a){a=Dd(a);a=new $c(a);for(var b=vd.w.slice(),c=0;c<a.w.length;c++)b[c]=b[c]||a.w[c];return(new $c(b)).encode()},Dd=function(a){a=a.get(Gd);ka(a)||(a=[]);return a};var ea=function(a){return"function"==typeof a},ka=function(a){return"[object Array]"==Object.prototype.toString.call(Object(a))},qa=function(a){return void 0!=a&&-1<(a.constructor+"").indexOf("String")},D=function(a,b){return 0==a.indexOf(b)},sa=function(a){return a?a.replace(/^[\s\xa0]+|[\s\xa0]+$/g,""):""},ra=function(){for(var a=O.navigator.userAgent+(M.cookie?M.cookie:"")+(M.referrer?M.referrer:""),b=a.length,c=O.history.length;0<c;)a+=c--^b++;return[hd()^La(a)&2147483647,Math.round((new Date).getTime()/
1E3)].join(".")},ta=function(a){var b=M.createElement("img");b.width=1;b.height=1;b.src=a;return b},ua=function(){},K=function(a){if(encodeURIComponent instanceof Function)return encodeURIComponent(a);J(28);return a},L=function(a,b,c,d){try{a.addEventListener?a.addEventListener(b,c,!!d):a.attachEvent&&a.attachEvent("on"+b,c)}catch(e){J(27)}},f=/^[\w\-:/.?=&%!]+$/,wa=function(a,b,c){a&&(c?(c="",b&&f.test(b)&&(c=' id="'+b+'"'),f.test(a)&&M.write("<script"+c+' src="'+a+'">\x3c/script>')):(c=M.createElement("script"),
c.type="text/javascript",c.async=!0,c.src=a,b&&(c.id=b),a=M.getElementsByTagName("script")[0],a.parentNode.insertBefore(c,a)))},be=function(a,b){return E(M.location[b?"href":"search"],a)},E=function(a,b){return(a=a.match("(?:&|#|\\?)"+K(b).replace(/([.*+?^=!:${}()|\[\]\/\\])/g,"\\$1")+"=([^&#]*)"))&&2==a.length?a[1]:""},xa=function(){var a=""+M.location.hostname;return 0==a.indexOf("www.")?a.substring(4):a},de=function(a,b){var c=a.indexOf(b);if(5==c||6==c)if(a=a.charAt(c+b.length),"/"==a||"?"==a||
""==a||":"==a)return!0;return!1},ya=function(a,b){var c=M.referrer;if(/^(https?|android-app):\/\//i.test(c)){if(a)return c;a="//"+M.location.hostname;if(!de(c,a))return b&&(b=a.replace(/\./g,"-")+".cdn.ampproject.org",de(c,b))?void 0:c}},za=function(a,b){if(1==b.length&&null!=b[0]&&"object"===typeof b[0])return b[0];for(var c={},d=Math.min(a.length+1,b.length),e=0;e<d;e++)if("object"===typeof b[e]){for(var g in b[e])b[e].hasOwnProperty(g)&&(c[g]=b[e][g]);break}else e<a.length&&(c[a[e]]=b[e]);return c};var ee=function(){this.keys=[];this.values={};this.m={}};ee.prototype.set=function(a,b,c){this.keys.push(a);c?this.m[":"+a]=b:this.values[":"+a]=b};ee.prototype.get=function(a){return this.m.hasOwnProperty(":"+a)?this.m[":"+a]:this.values[":"+a]};ee.prototype.map=function(a){for(var b=0;b<this.keys.length;b++){var c=this.keys[b],d=this.get(c);d&&a(c,d)}};var O=window,M=document,va=function(a,b){return setTimeout(a,b)};var F=window,Ea=document,G=function(a){var b=F._gaUserPrefs;if(b&&b.ioo&&b.ioo()||a&&!0===F["ga-disable-"+a])return!0;try{var c=F.external;if(c&&c._gaUserPrefs&&"oo"==c._gaUserPrefs)return!0}catch(g){}a=[];b=Ea.cookie.split(";");c=/^\s*AMP_TOKEN=\s*(.*?)\s*$/;for(var d=0;d<b.length;d++){var e=b[d].match(c);e&&a.push(e[1])}for(b=0;b<a.length;b++)if("$OPT_OUT"==decodeURIComponent(a[b]))return!0;return!1};var Ca=function(a){var b=[],c=M.cookie.split(";");a=new RegExp("^\\s*"+a+"=\\s*(.*?)\\s*$");for(var d=0;d<c.length;d++){var e=c[d].match(a);e&&b.push(e[1])}return b},zc=function(a,b,c,d,e,g){e=G(e)?!1:eb.test(M.location.hostname)||"/"==c&&vc.test(d)?!1:!0;if(!e)return!1;b&&1200<b.length&&(b=b.substring(0,1200));c=a+"="+b+"; path="+c+"; ";g&&(c+="expires="+(new Date((new Date).getTime()+g)).toGMTString()+"; ");d&&"none"!==d&&(c+="domain="+d+";");d=M.cookie;M.cookie=c;if(!(d=d!=M.cookie))a:{a=Ca(a);
for(d=0;d<a.length;d++)if(b==a[d]){d=!0;break a}d=!1}return d},Cc=function(a){return encodeURIComponent?encodeURIComponent(a).replace(/\(/g,"%28").replace(/\)/g,"%29"):a},vc=/^(www\.)?google(\.com?)?(\.[a-z]{2})?$/,eb=/(^|\.)doubleclick\.net$/i;var oc,Id=/^.*Version\/?(\d+)[^\d].*$/i,ne=function(){if(void 0!==O.__ga4__)return O.__ga4__;if(void 0===oc){var a=O.navigator.userAgent;if(a){var b=a;try{b=decodeURIComponent(a)}catch(c){}if(a=!(0<=b.indexOf("Chrome"))&&!(0<=b.indexOf("CriOS"))&&(0<=b.indexOf("Safari/")||0<=b.indexOf("Safari,")))b=Id.exec(b),a=11<=(b?Number(b[1]):-1);oc=a}else oc=!1}return oc};var Fa,Ga,fb,Ab,ja=/^https?:\/\/[^/]*cdn\.ampproject\.org\//,Ub=[],ic=function(){Z.D([ua])},tc=function(a,b){var c=Ca("AMP_TOKEN");if(1<c.length)return J(55),!1;c=decodeURIComponent(c[0]||"");if("$OPT_OUT"==c||"$ERROR"==c||G(b))return J(62),!1;if(!ja.test(M.referrer)&&"$NOT_FOUND"==c)return J(68),!1;if(void 0!==Ab)return J(56),va(function(){a(Ab)},0),!0;if(Fa)return Ub.push(a),!0;if("$RETRIEVING"==c)return J(57),va(function(){tc(a,b)},1E4),!0;Fa=!0;c&&"$"!=c[0]||(xc("$RETRIEVING",3E4),setTimeout(Mc,
3E4),c="");return Pc(c,b)?(Ub.push(a),!0):!1},Pc=function(a,b,c){if(!window.JSON)return J(58),!1;var d=O.XMLHttpRequest;if(!d)return J(59),!1;var e=new d;if(!("withCredentials"in e))return J(60),!1;e.open("POST",(c||"https://ampcid.google.com/v1/publisher:getClientId")+"?key=AIzaSyA65lEHUEizIsNtlbNo-l2K18dT680nsaM",!0);e.withCredentials=!0;e.setRequestHeader("Content-Type","text/plain");e.onload=function(){Fa=!1;if(4==e.readyState){try{200!=e.status&&(J(61),Qc("","$ERROR",3E4));var d=JSON.parse(e.responseText);
d.optOut?(J(63),Qc("","$OPT_OUT",31536E6)):d.clientId?Qc(d.clientId,d.securityToken,31536E6):!c&&d.alternateUrl?(Ga&&clearTimeout(Ga),Fa=!0,Pc(a,b,d.alternateUrl)):(J(64),Qc("","$NOT_FOUND",36E5))}catch(ca){J(65),Qc("","$ERROR",3E4)}e=null}};d={originScope:"AMP_ECID_GOOGLE"};a&&(d.securityToken=a);e.send(JSON.stringify(d));Ga=va(function(){J(66);Qc("","$ERROR",3E4)},1E4);return!0},Mc=function(){Fa=!1},xc=function(a,b){if(void 0===fb){fb="";for(var c=id(),d=0;d<c.length;d++){var e=c[d];if(zc("AMP_TOKEN",
encodeURIComponent(a),"/",e,"",b)){fb=e;return}}}zc("AMP_TOKEN",encodeURIComponent(a),"/",fb,"",b)},Qc=function(a,b,c){Ga&&clearTimeout(Ga);b&&xc(b,c);Ab=a;b=Ub;Ub=[];for(c=0;c<b.length;c++)b[c](a)};var oe=function(){return(Ba||"https:"==M.location.protocol?"https:":"http:")+"//www.google-analytics.com"},Da=function(a){this.name="len";this.message=a+"-8192"},ba=function(a,b,c){c=c||ua;if(2036>=b.length)wc(a,b,c);else if(8192>=b.length)x(a,b,c)||wd(a,b,c)||wc(a,b,c);else throw ge("len",b.length),new Da(b.length);},pe=function(a,b,c,d){d=d||ua;wd(a+"?"+b,"",d,c)},wc=function(a,b,c){var d=ta(a+"?"+b);d.onload=d.onerror=function(){d.onload=null;d.onerror=null;c()}},wd=function(a,b,c,d){var e=O.XMLHttpRequest;
if(!e)return!1;var g=new e;if(!("withCredentials"in g))return!1;a=a.replace(/^http:/,"https:");g.open("POST",a,!0);g.withCredentials=!0;g.setRequestHeader("Content-Type","text/plain");g.onreadystatechange=function(){if(4==g.readyState){if(d)try{var a=g.responseText;if(1>a.length)ge("xhr","ver","0"),c();else if("1"!=a.charAt(0))ge("xhr","ver",String(a.length)),c();else if(3<d.count++)ge("xhr","tmr",""+d.count),c();else if(1==a.length)c();else{var b=a.charAt(1);if("d"==b)pe("https://stats.g.doubleclick.net/j/collect",
d.U,d,c);else if("g"==b){var e="https://www.google.%/ads/ga-audiences".replace("%","com");wc(e,d.google,c);var w=a.substring(2);if(w)if(/^[a-z.]{1,6}$/.test(w)){var ha="https://www.google.%/ads/ga-audiences".replace("%",w);wc(ha,d.google,ua)}else ge("tld","bcc",w)}else ge("xhr","brc",b),c()}}catch(ue){ge("xhr","rsp"),c()}else c();g=null}};g.send(b);return!0},x=function(a,b,c){return O.navigator.sendBeacon?O.navigator.sendBeacon(a,b)?(c(),!0):!1:!1},ge=function(a,b,c){1<=100*Math.random()||G("?")||
(a=["t=error","_e="+a,"_v=j68","sr=1"],b&&a.push("_f="+b),c&&a.push("_m="+K(c.substring(0,100))),a.push("aip=1"),a.push("z="+hd()),wc("https://www.google-analytics.com/u/d",a.join("&"),ua))};var h=function(a){var b=O.gaData=O.gaData||{};return b[a]=b[a]||{}};var Ha=function(){this.M=[]};Ha.prototype.add=function(a){this.M.push(a)};Ha.prototype.D=function(a){try{for(var b=0;b<this.M.length;b++){var c=a.get(this.M[b]);c&&ea(c)&&c.call(O,a)}}catch(d){}b=a.get(Ia);b!=ua&&ea(b)&&(a.set(Ia,ua,!0),setTimeout(b,10))};function Ja(a){if(100!=a.get(Ka)&&La(P(a,Q))%1E4>=100*R(a,Ka))throw"abort";}function Ma(a){if(G(P(a,Na)))throw"abort";}function Oa(){var a=M.location.protocol;if("http:"!=a&&"https:"!=a)throw"abort";}
function Pa(a){try{O.navigator.sendBeacon?J(42):O.XMLHttpRequest&&"withCredentials"in new O.XMLHttpRequest&&J(40)}catch(c){}a.set(ld,Td(a),!0);a.set(Ac,R(a,Ac)+1);var b=[];Qa.map(function(c,d){d.F&&(c=a.get(c),void 0!=c&&c!=d.defaultValue&&("boolean"==typeof c&&(c*=1),b.push(d.F+"="+K(""+c))))});b.push("z="+Bd());a.set(Ra,b.join("&"),!0)}
function Sa(a){var b=P(a,gd)||oe()+"/collect",c=a.get(qe),d=P(a,fa);!d&&a.get(Vd)&&(d="beacon");if(c)pe(b,P(a,Ra),c,a.get(Ia));else if(d){c=d;d=P(a,Ra);var e=a.get(Ia);e=e||ua;"image"==c?wc(b,d,e):"xhr"==c&&wd(b,d,e)||"beacon"==c&&x(b,d,e)||ba(b,d,e)}else ba(b,P(a,Ra),a.get(Ia));b=a.get(Na);b=h(b);c=b.hitcount;b.hitcount=c?c+1:1;b=a.get(Na);delete h(b).pending_experiments;a.set(Ia,ua,!0)}
function Hc(a){(O.gaData=O.gaData||{}).expId&&a.set(Nc,(O.gaData=O.gaData||{}).expId);(O.gaData=O.gaData||{}).expVar&&a.set(Oc,(O.gaData=O.gaData||{}).expVar);var b=a.get(Na);if(b=h(b).pending_experiments){var c=[];for(d in b)b.hasOwnProperty(d)&&b[d]&&c.push(encodeURIComponent(d)+"."+encodeURIComponent(b[d]));var d=c.join("!")}else d=void 0;d&&a.set(m,d,!0)}function cd(){if(O.navigator&&"preview"==O.navigator.loadPurpose)throw"abort";}
function yd(a){var b=O.gaDevIds;ka(b)&&0!=b.length&&a.set("&did",b.join(","),!0)}function vb(a){if(!a.get(Na))throw"abort";};var hd=function(){return Math.round(2147483647*Math.random())},Bd=function(){try{var a=new Uint32Array(1);O.crypto.getRandomValues(a);return a[0]&2147483647}catch(b){return hd()}};function Ta(a){var b=R(a,Ua);500<=b&&J(15);var c=P(a,Va);if("transaction"!=c&&"item"!=c){c=R(a,Wa);var d=(new Date).getTime(),e=R(a,Xa);0==e&&a.set(Xa,d);e=Math.round(2*(d-e)/1E3);0<e&&(c=Math.min(c+e,20),a.set(Xa,d));if(0>=c)throw"abort";a.set(Wa,--c)}a.set(Ua,++b)};var Ya=function(){this.data=new ee},Qa=new ee,Za=[];Ya.prototype.get=function(a){var b=$a(a),c=this.data.get(a);b&&void 0==c&&(c=ea(b.defaultValue)?b.defaultValue():b.defaultValue);return b&&b.Z?b.Z(this,a,c):c};var P=function(a,b){a=a.get(b);return void 0==a?"":""+a},R=function(a,b){a=a.get(b);return void 0==a||""===a?0:1*a};Ya.prototype.set=function(a,b,c){if(a)if("object"==typeof a)for(var d in a)a.hasOwnProperty(d)&&ab(this,d,a[d],c);else ab(this,a,b,c)};
var ab=function(a,b,c,d){if(void 0!=c)switch(b){case Na:wb.test(c)}var e=$a(b);e&&e.o?e.o(a,b,c,d):a.data.set(b,c,d)},bb=function(a,b,c,d,e){this.name=a;this.F=b;this.Z=d;this.o=e;this.defaultValue=c},$a=function(a){var b=Qa.get(a);if(!b)for(var c=0;c<Za.length;c++){var d=Za[c],e=d[0].exec(a);if(e){b=d[1](e);Qa.set(b.name,b);break}}return b},yc=function(a){var b;Qa.map(function(c,d){d.F==a&&(b=d)});return b&&b.name},S=function(a,b,c,d,e){a=new bb(a,b,c,d,e);Qa.set(a.name,a);return a.name},cb=function(a,
b){Za.push([new RegExp("^"+a+"$"),b])},T=function(a,b,c){return S(a,b,c,void 0,db)},db=function(){};var gb=qa(window.GoogleAnalyticsObject)&&sa(window.GoogleAnalyticsObject)||"ga",jd=/^(?:utma\.)?\d+\.\d+$/,kd=/^amp-[\w.-]{22,64}$/,Ba=!1,hb=T("apiVersion","v"),ib=T("clientVersion","_v");S("anonymizeIp","aip");var jb=S("adSenseId","a"),Va=S("hitType","t"),Ia=S("hitCallback"),Ra=S("hitPayload");S("nonInteraction","ni");S("currencyCode","cu");S("dataSource","ds");var Vd=S("useBeacon",void 0,!1),fa=S("transport");S("sessionControl","sc","");S("sessionGroup","sg");S("queueTime","qt");var Ac=S("_s","_s");
S("screenName","cd");var kb=S("location","dl",""),lb=S("referrer","dr"),mb=S("page","dp","");S("hostname","dh");var nb=S("language","ul"),ob=S("encoding","de");S("title","dt",function(){return M.title||void 0});cb("contentGroup([0-9]+)",function(a){return new bb(a[0],"cg"+a[1])});var pb=S("screenColors","sd"),qb=S("screenResolution","sr"),rb=S("viewportSize","vp"),sb=S("javaEnabled","je"),tb=S("flashVersion","fl");S("campaignId","ci");S("campaignName","cn");S("campaignSource","cs");
S("campaignMedium","cm");S("campaignKeyword","ck");S("campaignContent","cc");var ub=S("eventCategory","ec"),xb=S("eventAction","ea"),yb=S("eventLabel","el"),zb=S("eventValue","ev"),Bb=S("socialNetwork","sn"),Cb=S("socialAction","sa"),Db=S("socialTarget","st"),Eb=S("l1","plt"),Fb=S("l2","pdt"),Gb=S("l3","dns"),Hb=S("l4","rrt"),Ib=S("l5","srt"),Jb=S("l6","tcp"),Kb=S("l7","dit"),Lb=S("l8","clt"),Mb=S("timingCategory","utc"),Nb=S("timingVar","utv"),Ob=S("timingLabel","utl"),Pb=S("timingValue","utt");
S("appName","an");S("appVersion","av","");S("appId","aid","");S("appInstallerId","aiid","");S("exDescription","exd");S("exFatal","exf");var Nc=S("expId","xid"),Oc=S("expVar","xvar"),m=S("exp","exp"),Rc=S("_utma","_utma"),Sc=S("_utmz","_utmz"),Tc=S("_utmht","_utmht"),Ua=S("_hc",void 0,0),Xa=S("_ti",void 0,0),Wa=S("_to",void 0,20);cb("dimension([0-9]+)",function(a){return new bb(a[0],"cd"+a[1])});cb("metric([0-9]+)",function(a){return new bb(a[0],"cm"+a[1])});S("linkerParam",void 0,void 0,Bc,db);
var ld=S("usage","_u"),Gd=S("_um");S("forceSSL",void 0,void 0,function(){return Ba},function(a,b,c){J(34);Ba=!!c});var ed=S("_j1","jid"),ia=S("_j2","gjid");cb("\\&(.*)",function(a){var b=new bb(a[0],a[1]),c=yc(a[0].substring(1));c&&(b.Z=function(a){return a.get(c)},b.o=function(a,b,g,ca){a.set(c,g,ca)},b.F=void 0);return b});
var Qb=T("_oot"),dd=S("previewTask"),Rb=S("checkProtocolTask"),md=S("validationTask"),Sb=S("checkStorageTask"),Uc=S("historyImportTask"),Tb=S("samplerTask"),Vb=S("_rlt"),Wb=S("buildHitTask"),Xb=S("sendHitTask"),Vc=S("ceTask"),zd=S("devIdTask"),Cd=S("timingTask"),Ld=S("displayFeaturesTask"),oa=S("customTask"),V=T("name"),Q=T("clientId","cid"),n=T("clientIdTime"),xd=T("storedClientId"),Ad=S("userId","uid"),Na=T("trackingId","tid"),U=T("cookieName",void 0,"_ga"),W=T("cookieDomain"),Yb=T("cookiePath",
void 0,"/"),Zb=T("cookieExpires",void 0,63072E3),Hd=T("cookieUpdate",void 0,!0),$b=T("legacyCookieDomain"),Wc=T("legacyHistoryImport",void 0,!0),ac=T("storage",void 0,"cookie"),bc=T("allowLinker",void 0,!1),cc=T("allowAnchor",void 0,!0),Ka=T("sampleRate","sf",100),dc=T("siteSpeedSampleRate",void 0,1),ec=T("alwaysSendReferrer",void 0,!1),I=T("_gid","_gid"),la=T("_gcn"),Kd=T("useAmpClientId"),ce=T("_gclid"),fe=T("_gt"),he=T("_ge",void 0,7776E6),ie=T("_gclsrc"),je=T("storeGac",void 0,!0),gd=S("transportUrl"),
Md=S("_r","_r"),qe=S("_dp"),Ud=S("allowAdFeatures",void 0,!0);function X(a,b,c,d){b[a]=function(){try{return d&&J(d),c.apply(this,arguments)}catch(e){throw ge("exc",a,e&&e.name),e;}}};var Od=function(){this.V=100;this.$=this.fa=!1;this.oa="detourexp";this.groups=1},Ed=function(a){var b=new Od,c;if(b.fa&&b.$)return 0;b.$=!0;if(a){if(b.oa&&void 0!==a.get(b.oa))return R(a,b.oa);if(0==a.get(dc))return 0}if(0==b.V)return 0;void 0===c&&(c=Bd());return 0==c%b.V?Math.floor(c/b.V)%b.groups+1:0};function fc(){var a,b;if((b=(b=O.navigator)?b.plugins:null)&&b.length)for(var c=0;c<b.length&&!a;c++){var d=b[c];-1<d.name.indexOf("Shockwave Flash")&&(a=d.description)}if(!a)try{var e=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");a=e.GetVariable("$version")}catch(g){}if(!a)try{e=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6"),a="WIN 6,0,21,0",e.AllowScriptAccess="always",a=e.GetVariable("$version")}catch(g){}if(!a)try{e=new ActiveXObject("ShockwaveFlash.ShockwaveFlash"),a=e.GetVariable("$version")}catch(g){}a&&
(e=a.match(/[\d]+/g))&&3<=e.length&&(a=e[0]+"."+e[1]+" r"+e[2]);return a||void 0};var aa=function(a){var b=Math.min(R(a,dc),100);return La(P(a,Q))%100>=b?!1:!0},gc=function(a){var b={};if(Ec(b)||Fc(b)){var c=b[Eb];void 0==c||Infinity==c||isNaN(c)||(0<c?(Y(b,Gb),Y(b,Jb),Y(b,Ib),Y(b,Fb),Y(b,Hb),Y(b,Kb),Y(b,Lb),va(function(){a(b)},10)):L(O,"load",function(){gc(a)},!1))}},Ec=function(a){var b=O.performance||O.webkitPerformance;b=b&&b.timing;if(!b)return!1;var c=b.navigationStart;if(0==c)return!1;a[Eb]=b.loadEventStart-c;a[Gb]=b.domainLookupEnd-b.domainLookupStart;a[Jb]=b.connectEnd-
b.connectStart;a[Ib]=b.responseStart-b.requestStart;a[Fb]=b.responseEnd-b.responseStart;a[Hb]=b.fetchStart-c;a[Kb]=b.domInteractive-c;a[Lb]=b.domContentLoadedEventStart-c;return!0},Fc=function(a){if(O.top!=O)return!1;var b=O.external,c=b&&b.onloadT;b&&!b.isValidLoadTime&&(c=void 0);2147483648<c&&(c=void 0);0<c&&b.setPageReadyTime();if(void 0==c)return!1;a[Eb]=c;return!0},Y=function(a,b){var c=a[b];if(isNaN(c)||Infinity==c||0>c)a[b]=void 0},Fd=function(a){return function(b){if("pageview"==b.get(Va)&&
!a.I){a.I=!0;var c=aa(b),d=0<E(b.get(kb),"gclid").length;(c||d)&&gc(function(b){c&&a.send("timing",b);d&&a.send("adtiming",b)})}}};var hc=!1,mc=function(a){if("cookie"==P(a,ac)){if(a.get(Hd)||P(a,xd)!=P(a,Q)){var b=1E3*R(a,Zb);ma(a,Q,U,b)}ma(a,I,la,864E5);if(a.get(je)){var c=a.get(ce);if(c){var d=Math.min(R(a,he),1E3*R(a,Zb));d=Math.min(d,1E3*R(a,fe)+d-(new Date).getTime());a.data.set(he,d);b={};var e=a.get(fe),g=a.get(ie),ca=kc(P(a,Yb)),l=lc(P(a,W)),k=P(a,Na);g&&"aw.ds"!=g?b&&(b.ua=!0):(c=["1",e,Cc(c)].join("."),0<d&&(b&&(b.ta=!0),zc("_gac_"+Cc(k),c,ca,l,k,d)));le(b)}}else J(75);if(a="none"===lc(P(a,W)))a=M.location.hostname,
a=eb.test(a)||vc.test(a);a&&J(30)}},ma=function(a,b,c,d){var e=nd(a,b);if(e){c=P(a,c);var g=kc(P(a,Yb)),ca=lc(P(a,W)),l=P(a,Na);if("auto"!=ca)zc(c,e,g,ca,l,d)&&(hc=!0);else{J(32);for(var k=id(),w=0;w<k.length;w++)if(ca=k[w],a.data.set(W,ca),e=nd(a,b),zc(c,e,g,ca,l,d)){hc=!0;return}a.data.set(W,"auto")}}},nc=function(a){if("cookie"==P(a,ac)&&!hc&&(mc(a),!hc))throw"abort";},Yc=function(a){if(a.get(Wc)){var b=P(a,W),c=P(a,$b)||xa(),d=Xc("__utma",c,b);d&&(J(19),a.set(Tc,(new Date).getTime(),!0),a.set(Rc,
d.R),(b=Xc("__utmz",c,b))&&d.hash==b.hash&&a.set(Sc,b.R))}},nd=function(a,b){b=Cc(P(a,b));var c=lc(P(a,W)).split(".").length;a=jc(P(a,Yb));1<a&&(c+="-"+a);return b?["GA1",c,b].join("."):""},Xd=function(a,b){return na(b,P(a,W),P(a,Yb))},na=function(a,b,c){if(!a||1>a.length)J(12);else{for(var d=[],e=0;e<a.length;e++){var g=a[e];var ca=g.split(".");var l=ca.shift();("GA1"==l||"1"==l)&&1<ca.length?(g=ca.shift().split("-"),1==g.length&&(g[1]="1"),g[0]*=1,g[1]*=1,ca={H:g,s:ca.join(".")}):ca=kd.test(g)?
{H:[0,0],s:g}:void 0;ca&&d.push(ca)}if(1==d.length)return J(13),d[0].s;if(0==d.length)J(12);else{J(14);d=Gc(d,lc(b).split(".").length,0);if(1==d.length)return d[0].s;d=Gc(d,jc(c),1);1<d.length&&J(41);return d[0]&&d[0].s}}},Gc=function(a,b,c){for(var d=[],e=[],g,ca=0;ca<a.length;ca++){var l=a[ca];l.H[c]==b?d.push(l):void 0==g||l.H[c]<g?(e=[l],g=l.H[c]):l.H[c]==g&&e.push(l)}return 0<d.length?d:e},lc=function(a){return 0==a.indexOf(".")?a.substr(1):a},id=function(){var a=[],b=xa().split(".");if(4==b.length){var c=
b[b.length-1];if(parseInt(c,10)==c)return["none"]}for(c=b.length-2;0<=c;c--)a.push(b.slice(c).join("."));a.push("none");return a},kc=function(a){if(!a)return"/";1<a.length&&a.lastIndexOf("/")==a.length-1&&(a=a.substr(0,a.length-1));0!=a.indexOf("/")&&(a="/"+a);return a},jc=function(a){a=kc(a);return"/"==a?1:a.split("/").length},le=function(a){a.ta&&J(77);a.na&&J(74);a.pa&&J(73);a.ua&&J(69)};function Xc(a,b,c){"none"==b&&(b="");var d=[],e=Ca(a);a="__utma"==a?6:2;for(var g=0;g<e.length;g++){var ca=(""+e[g]).split(".");ca.length>=a&&d.push({hash:ca[0],R:e[g],O:ca})}if(0!=d.length)return 1==d.length?d[0]:Zc(b,d)||Zc(c,d)||Zc(null,d)||d[0]}function Zc(a,b){if(null==a)var c=a=1;else c=La(a),a=La(D(a,".")?a.substring(1):"."+a);for(var d=0;d<b.length;d++)if(b[d].hash==c||b[d].hash==a)return b[d]};var od=new RegExp(/^https?:\/\/([^\/:]+)/),pd=/(.*)([?&#])(?:_ga=[^&#]*)(?:&?)(.*)/,me=/(.*)([?&#])(?:_gac=[^&#]*)(?:&?)(.*)/;function Bc(a){var b=a.get(Q),c=a.get(I)||"";b="_ga=2."+K(pa(c+b,0)+"."+c+"-"+b);if((c=a.get(ce))&&a.get(je)){var d=R(a,fe);1E3*d+R(a,he)<=(new Date).getTime()?(J(76),a=""):(J(44),a="&_gac=1."+K([pa(c,0),d,c].join(".")))}else a="";return b+a}
function Ic(a,b){var c=new Date,d=O.navigator,e=d.plugins||[];a=[a,d.userAgent,c.getTimezoneOffset(),c.getYear(),c.getDate(),c.getHours(),c.getMinutes()+b];for(b=0;b<e.length;++b)a.push(e[b].description);return La(a.join("."))}function pa(a,b){var c=new Date,d=O.navigator,e=c.getHours()+Math.floor((c.getMinutes()+b)/60);return La([a,d.userAgent,d.language||"",c.getTimezoneOffset(),c.getYear(),c.getDate()+Math.floor(e/24),(24+e)%24,(60+c.getMinutes()+b)%60].join("."))}
var Dc=function(a){J(48);this.target=a;this.T=!1};Dc.prototype.ca=function(a,b){if(a.tagName){if("a"==a.tagName.toLowerCase()){a.href&&(a.href=qd(this,a.href,b));return}if("form"==a.tagName.toLowerCase())return rd(this,a)}if("string"==typeof a)return qd(this,a,b)};
var qd=function(a,b,c){var d=pd.exec(b);d&&3<=d.length&&(b=d[1]+(d[3]?d[2]+d[3]:""));(d=me.exec(b))&&3<=d.length&&(b=d[1]+(d[3]?d[2]+d[3]:""));a=a.target.get("linkerParam");var e=b.indexOf("?");d=b.indexOf("#");c?b+=(-1==d?"#":"&")+a:(c=-1==e?"?":"&",b=-1==d?b+(c+a):b.substring(0,d)+c+a+b.substring(d));b=b.replace(/&+_ga=/,"&_ga=");return b=b.replace(/&+_gac=/,"&_gac=")},rd=function(a,b){if(b&&b.action)if("get"==b.method.toLowerCase()){a=a.target.get("linkerParam").split("&");for(var c=0;c<a.length;c++){var d=
a[c].split("="),e=d[1];d=d[0];for(var g=b.childNodes||[],ca=!1,l=0;l<g.length;l++)if(g[l].name==d){g[l].setAttribute("value",e);ca=!0;break}ca||(g=M.createElement("input"),g.setAttribute("type","hidden"),g.setAttribute("name",d),g.setAttribute("value",e),b.appendChild(g))}}else"post"==b.method.toLowerCase()&&(b.action=qd(a,b.action))};
Dc.prototype.S=function(a,b,c){function d(c){try{c=c||O.event;a:{var d=c.target||c.srcElement;for(c=100;d&&0<c;){if(d.href&&d.nodeName.match(/^a(?:rea)?$/i)){var g=d;break a}d=d.parentNode;c--}g={}}("http:"==g.protocol||"https:"==g.protocol)&&sd(a,g.hostname||"")&&g.href&&(g.href=qd(e,g.href,b))}catch(k){J(26)}}var e=this;this.T||(this.T=!0,L(M,"mousedown",d,!1),L(M,"keyup",d,!1));c&&L(M,"submit",function(b){b=b||O.event;if((b=b.target||b.srcElement)&&b.action){var c=b.action.match(od);c&&sd(a,c[1])&&
rd(e,b)}})};function sd(a,b){if(b==M.location.hostname)return!1;for(var c=0;c<a.length;c++)if(a[c]instanceof RegExp){if(a[c].test(b))return!0}else if(0<=b.indexOf(a[c]))return!0;return!1}function ke(a,b){return b!=Ic(a,0)&&b!=Ic(a,-1)&&b!=Ic(a,-2)&&b!=pa(a,0)&&b!=pa(a,-1)&&b!=pa(a,-2)};var p=/^(GTM|OPT)-[A-Z0-9]+$/,q=/;_gaexp=[^;]*/g,r=/;((__utma=)|([^;=]+=GAX?\d+\.))[^;]*/g,Aa=/^https?:\/\/[\w\-.]+\.google.com(:\d+)?\/optimize\/opt-launch\.html\?.*$/,t=function(a){function b(a,b){b&&(c+="&"+a+"="+K(b))}var c="https://www.google-analytics.com/gtm/js?id="+K(a.id);"dataLayer"!=a.B&&b("l",a.B);b("t",a.target);b("cid",a.clientId);b("cidt",a.ka);b("gac",a.la);b("aip",a.ia);a.sync&&b("m","sync");b("cycle",a.G);a.qa&&b("gclid",a.qa);Aa.test(M.referrer)&&b("cb",String(hd()));return c};var Jd=function(a,b,c){this.aa=b;(b=c)||(b=(b=P(a,V))&&"t0"!=b?Wd.test(b)?"_gat_"+Cc(P(a,Na)):"_gat_"+Cc(b):"_gat");this.Y=b;this.ra=null},Rd=function(a,b){var c=b.get(Wb);b.set(Wb,function(b){Pd(a,b,ed);Pd(a,b,ia);var d=c(b);Qd(a,b);return d});var d=b.get(Xb);b.set(Xb,function(b){var c=d(b);if(se(b)){if(ne()!==H(a,b)){J(80);var e={U:re(a,b,1),google:re(a,b,2),count:0};pe("https://stats.g.doubleclick.net/j/collect",e.U,e)}else ta(re(a,b,0));b.set(ed,"",!0)}return c})},Pd=function(a,b,c){!1===b.get(Ud)||
b.get(c)||("1"==Ca(a.Y)[0]?b.set(c,"",!0):b.set(c,""+hd(),!0))},Qd=function(a,b){se(b)&&zc(a.Y,"1",b.get(Yb),b.get(W),b.get(Na),6E4)},se=function(a){return!!a.get(ed)&&a.get(Ud)},re=function(a,b,c){var d=new ee,e=function(a){$a(a).F&&d.set($a(a).F,b.get(a))};e(hb);e(ib);e(Na);e(Q);e(ed);if(0==c||1==c)e(Ad),e(ia),e(I);d.set($a(ld).F,Td(b));var g="";d.map(function(a,b){g+=K(a)+"=";g+=K(""+b)+"&"});g+="z="+hd();0==c?g=a.aa+g:1==c?g="t=dc&aip=1&_r=3&"+g:2==c&&(g="t=sr&aip=1&_r=4&slf_rd=1&"+g);return g},
H=function(a,b){null===a.ra&&(a.ra=1===Ed(b),a.ra&&J(33));return a.ra},Wd=/^gtm\d+$/;var fd=function(a,b){a=a.b;if(!a.get("dcLoaded")){var c=new $c(Dd(a));c.set(29);a.set(Gd,c.w);b=b||{};var d;b[U]&&(d=Cc(b[U]));b=new Jd(a,"https://stats.g.doubleclick.net/r/collect?t=dc&aip=1&_r=3&",d);Rd(b,a);a.set("dcLoaded",!0)}};var Sd=function(a){if(!a.get("dcLoaded")&&"cookie"==a.get(ac)){var b=new Jd(a);Pd(b,a,ed);Pd(b,a,ia);Qd(b,a);if(se(a)){var c=ne()!==H(b,a);a.set(Md,1,!0);c?(J(79),a.set(gd,oe()+"/j/collect",!0),a.set(qe,{U:re(b,a,1),google:re(b,a,2),count:0},!0)):a.set(gd,oe()+"/r/collect",!0)}}};var Lc=function(){var a=O.gaGlobal=O.gaGlobal||{};return a.hid=a.hid||hd()};var ad,bd=function(a,b,c){if(!ad){var d=M.location.hash;var e=O.name,g=/^#?gaso=([^&]*)/;if(e=(d=(d=d&&d.match(g)||e&&e.match(g))?d[1]:Ca("GASO")[0]||"")&&d.match(/^(?:!([-0-9a-z.]{1,40})!)?([-.\w]{10,1200})$/i))zc("GASO",""+d,c,b,a,0),window._udo||(window._udo=b),window._utcp||(window._utcp=c),a=e[1],wa("https://www.google.com/analytics/web/inpage/pub/inpage.js?"+(a?"prefix="+a+"&":"")+hd(),"_gasojs");ad=!0}};var wb=/^(UA|YT|MO|GP)-(\d+)-(\d+)$/,pc=function(a){function b(a,b){d.b.data.set(a,b)}function c(a,c){b(a,c);d.filters.add(a)}var d=this;this.b=new Ya;this.filters=new Ha;b(V,a[V]);b(Na,sa(a[Na]));b(U,a[U]);b(W,a[W]||xa());b(Yb,a[Yb]);b(Zb,a[Zb]);b(Hd,a[Hd]);b($b,a[$b]);b(Wc,a[Wc]);b(bc,a[bc]);b(cc,a[cc]);b(Ka,a[Ka]);b(dc,a[dc]);b(ec,a[ec]);b(ac,a[ac]);b(Ad,a[Ad]);b(n,a[n]);b(Kd,a[Kd]);b(je,a[je]);b(hb,1);b(ib,"j68");c(Qb,Ma);c(oa,ua);c(dd,cd);c(Rb,Oa);c(md,vb);c(Sb,nc);c(Uc,Yc);c(Tb,Ja);c(Vb,Ta);
c(Vc,Hc);c(zd,yd);c(Ld,Sd);c(Wb,Pa);c(Xb,Sa);c(Cd,Fd(this));Kc(this.b);Jc(this.b,a[Q]);this.b.set(jb,Lc());bd(this.b.get(Na),this.b.get(W),this.b.get(Yb))},Jc=function(a,b){var c=P(a,U);a.data.set(la,"_ga"==c?"_gid":c+"_gid");if("cookie"==P(a,ac)){hc=!1;c=Ca(P(a,U));c=Xd(a,c);if(!c){c=P(a,W);var d=P(a,$b)||xa();c=Xc("__utma",d,c);void 0!=c?(J(10),c=c.O[1]+"."+c.O[2]):c=void 0}c&&(hc=!0);if(d=c&&!a.get(Hd))if(d=c.split("."),2!=d.length)d=!1;else if(d=Number(d[1])){var e=R(a,Zb);d=d+e<(new Date).getTime()/
1E3}else d=!1;d&&(c=void 0);c&&(a.data.set(xd,c),a.data.set(Q,c),c=Ca(P(a,la)),(c=Xd(a,c))&&a.data.set(I,c));if(a.get(je)&&(c=a.get(ce),d=a.get(ie),!c||d&&"aw.ds"!=d)){c={};if(M){d=[];e=M.cookie.split(";");for(var g=/^\s*_gac_(UA-\d+-\d+)=\s*(.+?)\s*$/,ca=0;ca<e.length;ca++){var l=e[ca].match(g);l&&d.push({ja:l[1],value:l[2]})}e={};if(d&&d.length)for(g=0;g<d.length;g++)(ca=d[g].value.split("."),"1"!=ca[0]||3!=ca.length)?c&&(c.na=!0):ca[1]&&(e[d[g].ja]?c&&(c.pa=!0):e[d[g].ja]=[],e[d[g].ja].push({timestamp:ca[1],
qa:ca[2]}));d=e}else d={};d=d[P(a,Na)];le(c);d&&0!=d.length&&(c=d[0],a.data.set(fe,c.timestamp),a.data.set(ce,c.qa))}}if(a.get(Hd))a:if(d=be("_ga",a.get(cc)))if(a.get(bc))if(c=d.indexOf("."),-1==c)J(22);else{e=d.substring(0,c);g=d.substring(c+1);c=g.indexOf(".");d=g.substring(0,c);g=g.substring(c+1);if("1"==e){if(c=g,ke(c,d)){J(23);break a}}else if("2"==e){c=g.indexOf("-");e="";0<c?(e=g.substring(0,c),c=g.substring(c+1)):c=g.substring(1);if(ke(e+c,d)){J(53);break a}e&&(J(2),a.data.set(I,e))}else{J(22);
break a}J(11);a.data.set(Q,c);if(c=be("_gac",a.get(cc)))c=c.split("."),"1"!=c[0]||4!=c.length?J(72):ke(c[3],c[1])?J(71):(a.data.set(ce,c[3]),a.data.set(fe,c[2]),J(70))}else J(21);b&&(J(9),a.data.set(Q,K(b)));a.get(Q)||((b=(b=O.gaGlobal&&O.gaGlobal.vid)&&-1!=b.search(jd)?b:void 0)?(J(17),a.data.set(Q,b)):(J(8),a.data.set(Q,ra())));a.get(I)||(J(3),a.data.set(I,ra()));mc(a)},Kc=function(a){var b=O.navigator,c=O.screen,d=M.location;a.set(lb,ya(a.get(ec),a.get(Kd)));if(d){var e=d.pathname||"";"/"!=e.charAt(0)&&
(J(31),e="/"+e);a.set(kb,d.protocol+"//"+d.hostname+e+d.search)}c&&a.set(qb,c.width+"x"+c.height);c&&a.set(pb,c.colorDepth+"-bit");c=M.documentElement;var g=(e=M.body)&&e.clientWidth&&e.clientHeight,ca=[];c&&c.clientWidth&&c.clientHeight&&("CSS1Compat"===M.compatMode||!g)?ca=[c.clientWidth,c.clientHeight]:g&&(ca=[e.clientWidth,e.clientHeight]);c=0>=ca[0]||0>=ca[1]?"":ca.join("x");a.set(rb,c);a.set(tb,fc());a.set(ob,M.characterSet||M.charset);a.set(sb,b&&"function"===typeof b.javaEnabled&&b.javaEnabled()||
!1);a.set(nb,(b&&(b.language||b.browserLanguage)||"").toLowerCase());a.data.set(ce,be("gclid",!0));a.data.set(ie,be("gclsrc",!0));a.data.set(fe,Math.round((new Date).getTime()/1E3));if(d&&a.get(cc)&&(b=M.location.hash)){b=b.split(/[?&#]+/);d=[];for(c=0;c<b.length;++c)(D(b[c],"utm_id")||D(b[c],"utm_campaign")||D(b[c],"utm_source")||D(b[c],"utm_medium")||D(b[c],"utm_term")||D(b[c],"utm_content")||D(b[c],"gclid")||D(b[c],"dclid")||D(b[c],"gclsrc"))&&d.push(b[c]);0<d.length&&(b="#"+d.join("&"),a.set(kb,
a.get(kb)+b))}};pc.prototype.get=function(a){return this.b.get(a)};pc.prototype.set=function(a,b){this.b.set(a,b)};var qc={pageview:[mb],event:[ub,xb,yb,zb],social:[Bb,Cb,Db],timing:[Mb,Nb,Pb,Ob]};pc.prototype.send=function(a){if(!(1>arguments.length)){if("string"===typeof arguments[0]){var b=arguments[0];var c=[].slice.call(arguments,1)}else b=arguments[0]&&arguments[0][Va],c=arguments;b&&(c=za(qc[b]||[],c),c[Va]=b,this.b.set(c,void 0,!0),this.filters.D(this.b),this.b.data.m={})}};
pc.prototype.ma=function(a,b){var c=this;u(a,c,b)||(v(a,function(){u(a,c,b)}),y(String(c.get(V)),a,void 0,b,!0))};var rc=function(a){if("prerender"==M.visibilityState)return!1;a();return!0},z=function(a){if(!rc(a)){J(16);var b=!1,c=function(){if(!b&&rc(a)){b=!0;var d=c,e=M;e.removeEventListener?e.removeEventListener("visibilitychange",d,!1):e.detachEvent&&e.detachEvent("onvisibilitychange",d)}};L(M,"visibilitychange",c)}};var td=/^(?:(\w+)\.)?(?:(\w+):)?(\w+)$/,sc=function(a){if(ea(a[0]))this.u=a[0];else{var b=td.exec(a[0]);null!=b&&4==b.length&&(this.c=b[1]||"t0",this.K=b[2]||"",this.C=b[3],this.a=[].slice.call(a,1),this.K||(this.A="create"==this.C,this.i="require"==this.C,this.g="provide"==this.C,this.ba="remove"==this.C),this.i&&(3<=this.a.length?(this.X=this.a[1],this.W=this.a[2]):this.a[1]&&(qa(this.a[1])?this.X=this.a[1]:this.W=this.a[1])));b=a[1];a=a[2];if(!this.C)throw"abort";if(this.i&&(!qa(b)||""==b))throw"abort";
if(this.g&&(!qa(b)||""==b||!ea(a)))throw"abort";if(ud(this.c)||ud(this.K))throw"abort";if(this.g&&"t0"!=this.c)throw"abort";}};function ud(a){return 0<=a.indexOf(".")||0<=a.indexOf(":")};var Yd,Zd,$d,A;Yd=new ee;$d=new ee;A=new ee;Zd={ec:45,ecommerce:46,linkid:47};
var u=function(a,b,c){b==N||b.get(V);var d=Yd.get(a);if(!ea(d))return!1;b.plugins_=b.plugins_||new ee;if(b.plugins_.get(a))return!0;b.plugins_.set(a,new d(b,c||{}));return!0},y=function(a,b,c,d,e){if(!ea(Yd.get(b))&&!$d.get(b)){Zd.hasOwnProperty(b)&&J(Zd[b]);if(p.test(b)){J(52);a=N.j(a);if(!a)return!0;c=d||{};d={id:b,B:c.dataLayer||"dataLayer",ia:!!a.get("anonymizeIp"),sync:e,G:!1};a.get("&gtm")==b&&(d.G=!0);var g=String(a.get("name"));"t0"!=g&&(d.target=g);G(String(a.get("trackingId")))||(d.clientId=
String(a.get(Q)),d.ka=Number(a.get(n)),c=c.palindrome?r:q,c=(c=M.cookie.replace(/^|(; +)/g,";").match(c))?c.sort().join("").substring(1):void 0,d.la=c,d.qa=E(a.b.get(kb)||"","gclid"));a=d.B;c=(new Date).getTime();O[a]=O[a]||[];c={"gtm.start":c};e||(c.event="gtm.js");O[a].push(c);c=t(d)}!c&&Zd.hasOwnProperty(b)?(J(39),c=b+".js"):J(43);c&&(c&&0<=c.indexOf("/")||(c=(Ba||"https:"==M.location.protocol?"https:":"http:")+"//www.google-analytics.com/plugins/ua/"+c),d=ae(c),a=d.protocol,c=M.location.protocol,
("https:"==a||a==c||("http:"!=a?0:"http:"==c))&&B(d)&&(wa(d.url,void 0,e),$d.set(b,!0)))}},v=function(a,b){var c=A.get(a)||[];c.push(b);A.set(a,c)},C=function(a,b){Yd.set(a,b);b=A.get(a)||[];for(var c=0;c<b.length;c++)b[c]();A.set(a,[])},B=function(a){var b=ae(M.location.href);if(D(a.url,"https://www.google-analytics.com/gtm/js?id="))return!0;if(a.query||0<=a.url.indexOf("?")||0<=a.path.indexOf("://"))return!1;if(a.host==b.host&&a.port==b.port)return!0;b="http:"==a.protocol?80:443;return"www.google-analytics.com"==
a.host&&(a.port||b)==b&&D(a.path,"/plugins/")?!0:!1},ae=function(a){function b(a){var b=(a.hostname||"").split(":")[0].toLowerCase(),c=(a.protocol||"").toLowerCase();c=1*a.port||("http:"==c?80:"https:"==c?443:"");a=a.pathname||"";D(a,"/")||(a="/"+a);return[b,""+c,a]}var c=M.createElement("a");c.href=M.location.href;var d=(c.protocol||"").toLowerCase(),e=b(c),g=c.search||"",ca=d+"//"+e[0]+(e[1]?":"+e[1]:"");D(a,"//")?a=d+a:D(a,"/")?a=ca+a:!a||D(a,"?")?a=ca+e[2]+(a||g):0>a.split("/")[0].indexOf(":")&&
(a=ca+e[2].substring(0,e[2].lastIndexOf("/"))+"/"+a);c.href=a;d=b(c);return{protocol:(c.protocol||"").toLowerCase(),host:d[0],port:d[1],path:d[2],query:c.search||"",url:a||""}};var Z={ga:function(){Z.f=[]}};Z.ga();Z.D=function(a){var b=Z.J.apply(Z,arguments);b=Z.f.concat(b);for(Z.f=[];0<b.length&&!Z.v(b[0])&&!(b.shift(),0<Z.f.length););Z.f=Z.f.concat(b)};Z.J=function(a){for(var b=[],c=0;c<arguments.length;c++)try{var d=new sc(arguments[c]);d.g?C(d.a[0],d.a[1]):(d.i&&(d.ha=y(d.c,d.a[0],d.X,d.W)),b.push(d))}catch(e){}return b};
Z.v=function(a){try{if(a.u)a.u.call(O,N.j("t0"));else{var b=a.c==gb?N:N.j(a.c);if(a.A){if("t0"==a.c&&(b=N.create.apply(N,a.a),null===b))return!0}else if(a.ba)N.remove(a.c);else if(b)if(a.i){if(a.ha&&(a.ha=y(a.c,a.a[0],a.X,a.W)),!u(a.a[0],b,a.W))return!0}else if(a.K){var c=a.C,d=a.a,e=b.plugins_.get(a.K);e[c].apply(e,d)}else b[a.C].apply(b,a.a)}}catch(g){}};var N=function(a){J(1);Z.D.apply(Z,[arguments])};N.h={};N.P=[];N.L=0;N.answer=42;var uc=[Na,W,V];
N.create=function(a){var b=za(uc,[].slice.call(arguments));b[V]||(b[V]="t0");var c=""+b[V];if(N.h[c])return N.h[c];a:{if(b[Kd]){J(67);if(b[ac]&&"cookie"!=b[ac]){var d=!1;break a}if(void 0!==Ab)b[Q]||(b[Q]=Ab);else{b:{d=String(b[W]||xa());var e=String(b[Yb]||"/"),g=Ca(String(b[U]||"_ga"));d=na(g,d,e);if(!d||jd.test(d))d=!0;else if(d=Ca("AMP_TOKEN"),0==d.length)d=!0;else{if(1==d.length&&(d=decodeURIComponent(d[0]),"$RETRIEVING"==d||"$OPT_OUT"==d||"$ERROR"==d||"$NOT_FOUND"==d)){d=!0;break b}d=!1}}if(d&&
tc(ic,String(b[Na]))){d=!0;break a}}}d=!1}if(d)return null;b=new pc(b);N.h[c]=b;N.P.push(b);return b};N.remove=function(a){for(var b=0;b<N.P.length;b++)if(N.P[b].get(V)==a){N.P.splice(b,1);N.h[a]=null;break}};N.j=function(a){return N.h[a]};N.getAll=function(){return N.P.slice(0)};
N.N=function(){"ga"!=gb&&J(49);var a=O[gb];if(!a||42!=a.answer){N.L=a&&a.l;N.loaded=!0;var b=O[gb]=N;X("create",b,b.create);X("remove",b,b.remove);X("getByName",b,b.j,5);X("getAll",b,b.getAll,6);b=pc.prototype;X("get",b,b.get,7);X("set",b,b.set,4);X("send",b,b.send);X("requireSync",b,b.ma);b=Ya.prototype;X("get",b,b.get);X("set",b,b.set);if("https:"!=M.location.protocol&&!Ba){a:{b=M.getElementsByTagName("script");for(var c=0;c<b.length&&100>c;c++){var d=b[c].src;if(d&&0==d.indexOf("https://www.google-analytics.com/analytics")){b=
!0;break a}}b=!1}b&&(Ba=!0)}(O.gaplugins=O.gaplugins||{}).Linker=Dc;b=Dc.prototype;C("linker",Dc);X("decorate",b,b.ca,20);X("autoLink",b,b.S,25);C("displayfeatures",fd);C("adfeatures",fd);a=a&&a.q;ka(a)?Z.D.apply(N,a):J(50)}};N.da=function(){for(var a=N.getAll(),b=0;b<a.length;b++)a[b].get(V)};var da=N.N,Nd=O[gb];Nd&&Nd.r?da():z(da);z(function(){Z.D(["provide","render",ua])});function La(a){var b=1,c;if(a)for(b=0,c=a.length-1;0<=c;c--){var d=a.charCodeAt(c);b=(b<<6&268435455)+d+(d<<14);d=b&266338304;b=0!=d?b^d>>21:b}return b};})(window);

File diff suppressed because one or more lines are too long

View File

@@ -1,150 +0,0 @@
/**
* Site-wide JS that sets up:
*
* [1] MathJax rendering on navigation
* [2] Sidebar toggling
* [3] Sidebar scroll preserving
* [4] Keyboard navigation
* [5] Right sidebar scroll highlighting
*/
const togglerId = 'js-sidebar-toggle'
const textbookId = 'js-textbook'
const togglerActiveClass = 'is-active'
const textbookActiveClass = 'js-show-sidebar'
const mathRenderedClass = 'js-mathjax-rendered'
const icon_path = document.location.origin + `${site_basename}assets`;
const getToggler = () => document.getElementById(togglerId)
const getTextbook = () => document.getElementById(textbookId)
// [1] Run MathJax when Turbolinks navigates to a page.
// When Turbolinks caches a page, it also saves the MathJax rendering. We mark
// each page with a CSS class after rendering to prevent double renders when
// navigating back to a cached page.
document.addEventListener('turbolinks:load', () => {
const textbook = getTextbook()
if (window.MathJax && !textbook.classList.contains(mathRenderedClass)) {
MathJax.Hub.Queue(['Typeset', MathJax.Hub])
textbook.classList.add(mathRenderedClass)
}
})
/**
* [2] Toggles sidebar and menu icon
*/
const toggleSidebar = () => {
const toggler = getToggler()
const textbook = getTextbook()
if (textbook.classList.contains(textbookActiveClass)) {
textbook.classList.remove(textbookActiveClass)
toggler.classList.remove(togglerActiveClass)
} else {
textbook.classList.add(textbookActiveClass)
toggler.classList.add(togglerActiveClass)
}
}
/**
* Keep the variable below in sync with the tablet breakpoint value in
* _sass/inuitcss/tools/_tools.mq.scss
*
*/
const autoCloseSidebarBreakpoint = 740
// Set up event listener for sidebar toggle button
const sidebarButtonHandler = () => {
getToggler().addEventListener('click', toggleSidebar)
/**
* Auto-close sidebar on smaller screens after page load.
*
* Having the sidebar be open by default then closing it on page load for
* small screens gives the illusion that the sidebar closes in response
* to selecting a page in the sidebar. However, it does cause a bit of jank
* on the first page load.
*
* Since we don't want to persist state in between page navigation, this is
* the best we can do while optimizing for larger screens where most
* viewers will read the textbook.
*
* The code below assumes that the sidebar is open by default.
*/
if (window.innerWidth < autoCloseSidebarBreakpoint) toggleSidebar()
}
initFunction(sidebarButtonHandler);
/**
* [3] Preserve sidebar scroll when navigating between pages
*/
let sidebarScrollTop = 0
const getSidebar = () => document.getElementById('js-sidebar')
document.addEventListener('turbolinks:before-visit', () => {
sidebarScrollTop = getSidebar().scrollTop
})
document.addEventListener('turbolinks:load', () => {
getSidebar().scrollTop = sidebarScrollTop
})
/**
* Focus textbook page by default so that user can scroll with spacebar
*/
const focusPage = () => {
document.querySelector('.c-textbook__page').focus()
}
initFunction(focusPage);
/**
* [4] Use left and right arrow keys to navigate forward and backwards.
*/
const LEFT_ARROW_KEYCODE = 37
const RIGHT_ARROW_KEYCODE = 39
const getPrevUrl = () => document.getElementById('js-page__nav__prev').href
const getNextUrl = () => document.getElementById('js-page__nav__next').href
const initPageNav = (event) => {
const keycode = event.which
if (keycode === LEFT_ARROW_KEYCODE) {
Turbolinks.visit(getPrevUrl())
} else if (keycode === RIGHT_ARROW_KEYCODE) {
Turbolinks.visit(getNextUrl())
}
};
var keyboardListener = false;
const initListener = () => {
if (keyboardListener === false) {
document.addEventListener('keydown', initPageNav)
keyboardListener = true;
}
}
initFunction(initListener);
/**
* [5] Right sidebar scroll highlighting
*/
highlightRightSidebar = function() {
var position = document.querySelector('.c-textbook__page').scrollTop;
position = position + (window.innerHeight / 4); // + Manual offset
// Highlight the "active" menu item
document.querySelectorAll('.c-textbook__content h2, .c-textbook__content h3').forEach((header, index) => {
var target = header.offsetTop;
var id = header.id;
if (position >= target) {
var query = 'ul.toc__menu a[href="#' + id + '"]';
document.querySelectorAll('ul.toc__menu li').forEach((item) => {item.classList.remove('active')});
document.querySelectorAll(query).forEach((item) => {item.parentElement.classList.add('active')});
}
});
document.querySelector('.c-textbook__page').addEventListener('scroll', highlightRightSidebar);
};
initFunction(highlightRightSidebar);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -17,38 +17,31 @@
{
"cell_type": "markdown",
"source": [
"Migration Compatibility Assessment\n",
"=======================================================\n",
"# Migration Compatibility Assessment\n",
"Use dmacmd.exe to assess databases in an unattended mode, and output the result to JSON or CSV file. This method is especially useful when assessing several databases or huge databases.\n",
"\n",
"Description\n",
"-----------\n",
"Use this notebook to analzye an on-premises SQL Server instance or database for compatibility for migration to SQL Azure. The assessment will provide guidance on features not currently supported in Azure and remediation actions that can be taken to prepare for migration.\n",
""
"## Notebook Variables\n",
"\n",
"| Line | Variable | Description |\n",
"| --- | --- | --- |\n",
"| 1 | ExecutableFile | Path to DmaCmd.exe file, usually _\"C:\\\\Program Files\\\\Microsoft Data Migration Assistant\\\\DmaCmd.exe\"_ if installed to default location |\n",
"| 2 | AssessmentName | Unique name for assessment |\n",
"| 3 | Server | Target SQL Server |\n",
"| 4 | InitialCatalog | Name of the database for the specified server |\n",
"| 5 | ResultPath | Path and name of the file to store results in json format |"
],
"metadata": {
"azdata_cell_guid": "6764dd37-fb1f-400d-8f2b-70bc36fc3b61"
}
},
{
"cell_type": "markdown",
"source": [
"This notebook requires Data Migration Assistant to be installed in order to execute the below commands.\r\n",
"The installtion link would be [Data Migration Assistant download](https://www.microsoft.com/en-us/download/confirmation.aspx?id=53595)\r\n",
"\r\n",
"_With version 2.1 and above, when installation of Data Migration Assistant is successfull, it will also install dmacmd.exe in %ProgramFiles%\\Microsoft Data Migration Assistant\\. Use dmacmd.exe to assess databases in an unattended mode, and output the result to JSON or CSV file. This method is especially useful when assessing several databases or huge databases_"
],
"metadata": {
"azdata_cell_guid": "68506e39-d34b-4f17-a0c6-94e978f76488"
}
},
{
"cell_type": "code",
"source": [
"$ExecutableFile = \"\" # Path of the DmaCmd.exe file, generally the path would be \"C:\\Program Files\\Microsoft Data Migration Assistant\\DmaCmd.exe\"\r\n",
"$AssessmentName = \"\" # Name of the Assessment\r\n",
"$Server = \"\" # Targert Sql Server\r\n",
"$InitialCatalog = \"\" # Database name of the specified Sql Server\r\n",
"$ResultPath = \"\" # Path and Name of the file to store the result in json format, for example \"C:\\\\temp\\\\Results\\\\AssessmentReport.json\""
"$ExecutableFile = \"C:\\Program Files\\Microsoft Data Migration Assistant\\DmaCmd.exe\" # Update if different\r\n",
"$AssessmentName = \"\"\r\n",
"$Server = \"\"\r\n",
"$InitialCatalog = \"\"\r\n",
"$ResultPath = \"\""
],
"metadata": {
"azdata_cell_guid": "d81972c1-3b0b-47d9-b8a3-bc5ab4001a34"

View File

@@ -1,9 +1,10 @@
# Assessments
[Home](../readme.md)
## Notebooks in this Chapter
- [SQL Server Best Practices Assessment](sql-server-assessment.ipynb) - Use the SQL Server Assessment API to review the configuration of instances by name or dynamically by specifying the instance of a Central Management Server. SQL Assessment API provides a mechanism to evaluate the configuration of your SQL Server for best practices. The API is delivered with a ruleset containing best practice rules suggested by SQL Server Team. This ruleset is enhancing with the release of new versions but at the same time, the API is built with the intent to give a highly customizable and extensible solution. So, users can tune the default rules and create their own ones. SQL Assessment API is useful when you want to make sure your SQL Server configuration is in line with recommended best practices. After an initial assessment, configuration stability can be tracked by regularly scheduled assessments.
Preparing for the cloud requires a crawl-walk-run mentality. The first step, or crawl, towards hybrid migration is determining the fitness of existing on-premise resources. An assessment is an analysis performed against a chosen SQL Server object such as a Server or Database instance. It is recommended to fix any issues found by the analysis prior to migrating a database from on-premise to Azure.
- [Compatibility Assessment](compatibility-assessment.ipynb) - Coming soon
## Notebooks in this Chapter
- [SQL Server Best Practices Assessment](sql-server-assessment.ipynb) - demonstrates the use of the [SQL Server Assessment API](https://docs.microsoft.com/en-us/sql/sql-assessment-api/sql-assessment-api-overview), a tool to review the configuration of a SQL Server and Databases for best practices.
- [Compatibility Assessment](compatibility-assessment.ipynb) - Analzye an on-premises SQL Server instance or database for compatibility for migration to SQL Azure. The assessment will provide guidance on features not currently supported in Azure and remediation actions that can be taken to prepare for migration.

View File

@@ -19,17 +19,16 @@
"source": [
"# SQL Server Assessment Tool\n",
"\n",
"This notebook will demonstrate the use of the [SQL Server Assessment API](https://docs.microsoft.com/en-us/sql/sql-assessment-api/sql-assessment-api-overview), a tool to review the configuration of a SQL Server and Databases for best practices. An assessment is performed against a chosen SQL Server object. The default ruleset checks for two kinds of objects: Server and Database. In addition, the API supports Filegroup and AvailabilityGroup. When attempting to migrate a database from on-premise to Azure, it is recommended to fix any assessment items prior.\n",
"\n",
"**Unlike other notebooks, do not execute all cells of this notebook!** \n",
"Unlike other notebooks, **do not execute all cells of this notebook!** \n",
"\n",
"A single assessment may take awhile so fill out the variables and execute the cell that matches the desired environment to perform the assessment needed. Only one of these cells needs to be executed after the variables are defined.\n",
"\n",
"1. Ensure that the proper APIs and modules are installed per the <a href=\"../prereqs.ipynb\">prerequisites</a> notebook\n",
"2. Define a service instance and group corresponding to the SQL Server instances to be assessed\n",
"3. Choose an example below that corresponds to the appropriate task\n",
"4. Execute only that example's code block and wait for results\n",
"5. Fix any recommended issues and rerun Assessment API until clear"
"## Notebook Variables\n",
"\n",
"| Line | Variable | Description |\n",
"| ---- | -------- | ----------- |\n",
"| 1 | ServerInstance | Name of the SQL Server instance |\n",
"| 2 | Group | (Optional) Name of the server group, if known | "
],
"metadata": {
"azdata_cell_guid": "86ecfb01-8c38-4a99-92a8-687d8ec7f4b0"
@@ -47,6 +46,20 @@
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"## Notebook Steps\r\n",
"1. Ensure that the proper APIs and modules are installed per the <a href=\"../prereqs.ipynb\">prerequisites</a> notebook\r\n",
"2. Define a service instance and group corresponding to the SQL Server instances to be assessed\r\n",
"3. Choose an example below that corresponds to the appropriate task\r\n",
"4. Execute only that example's code block and wait for results\r\n",
"5. Fix any recommended issues and rerun Assessment API until clear"
],
"metadata": {
"azdata_cell_guid": "541f6806-f8d2-4fc5-a8fb-6d42947d1a64"
}
},
{
"cell_type": "markdown",
"source": [

View File

@@ -23,7 +23,7 @@
{
"cell_type": "code",
"source": [
"import pandas,sys,os,getpass,json,html,time\r\n",
"import sys,os,getpass,json,html,time\r\n",
"from string import Template"
],
"metadata": {

View File

@@ -1,8 +1,10 @@
# Appendices
[Home](readme.md)
## Appendix: Locations
See the <a href="https://azure.microsoft.com/en-us/global-infrastructure/locations/">Azure locations</a> page for a complete list of Azure regions along with their general physical location. The following is a list of common North American location settings for this guide:
### US Regions
### Regions
| Setting | Location |
| ------------ | --------- |
| Central US | Iowa |

View File

@@ -18,12 +18,46 @@
"cell_type": "markdown",
"source": [
"# Export Existing Azure SQL Server Resources\r\n",
"Export notebook that will utilize the ADP resources\r\n",
"\r\n",
"\r\n",
"<!-- Disable bullets to be shown for checkbox markup -->\r\n",
"<style type=\"text/css\">\r\n",
" ul { list-style-type: none }\r\n",
"</style>\r\n",
"## Notebook Variables\r\n",
"| Line | Variable | Description |\r\n",
"| -- | -- | -- |\r\n",
"| 1 | AdpSubscription | Azure Subscription ID/Name for the ADP Resource Group # Both RG are assumed to be in the same subscription |\r\n",
"| 2 | AdpResourceGroup | Azure Resource Group which contains the ADP Resources | \r\n",
"| 3 | SourceResourceGroup | Azure ResourceGroup where the sql server to be exported exists | \r\n",
"| 4 | LogicalSQLServerName | Logical sql server name of the sql server to be exported | \r\n",
"| 5 | StorageAccount | target storage account to store exported files # any storage account, but must be in the same RG as the ADP resources | \r\n",
"| 6 | AdpFunc | |\r\n",
"| 7 | AdpBatch | | \r\n",
"| 8 | AdpVNET | | "
],
"metadata": {
"azdata_cell_guid": "b72d138a-566f-4161-b7a6-7264487e446c"
}
},
{
"cell_type": "code",
"source": [
"$AdpSubscription = \"\"\r\n",
"$AdpResourceGroup = \"\"\r\n",
"$SourceResourceGroup= \"\"\r\n",
"$LogicalSQLServer = \"\"\r\n",
"$StorageAccount = \"\"\r\n",
"$AdpFunc = $AdpResourceGroup + \"Control\"\r\n",
"$AdpBatch = $AdpResourceGroup.ToLower() + \"batch\"\r\n",
"$AdpVNET = $AdpResourceGroup + \"Vnet\""
],
"metadata": {
"azdata_cell_guid": "417edc0e-1107-4a27-a4cf-e921f79b3f6a",
"tags": []
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
"## Steps\r\n",
"Gather input:\r\n",
"* [ ] Connect to Azure Subscription\r\n",
@@ -45,39 +79,6 @@
"azdata_cell_guid": "a9da248a-20f1-4574-bd04-7324e70c05a3"
}
},
{
"cell_type": "markdown",
"source": [
"## Set Variables for the Notebook"
],
"metadata": {
"azdata_cell_guid": "b72d138a-566f-4161-b7a6-7264487e446c"
}
},
{
"cell_type": "code",
"source": [
"# ADP Resource \r\n",
"$Env:BOOTSTRAP_Subscription = \"\" # Azure Subscription ID/Name for the ADP Resource Group # Both RG are assumed to be in the same subscription\r\n",
"$Env:BOOTSTRAP_ResourceGroup = \"\" # Azure Resource Group which contains the ADP Resources\r\n",
"\r\n",
"# SQL Server \r\n",
"$SourceResourceGroupName = \"\" # Azure ResourceGroup where the sql server to be exported exists\r\n",
"$LogicalSQLServerName = \"\" # Logical sql server name of the sql server to be exported\r\n",
"$StorageAccount = \"\" # target storage account to store exported files # any storage account, but must be in the same RG as the ADP resources.\r\n",
"\r\n",
"# Set Variables for ADP Resources\r\n",
"$Env:BOOTSTRAP_FUNC = $Env:BOOTSTRAP_ResourceGroup + \"Control\"\r\n",
"$Env:BOOTSTRAP_BATCH = $Env:BOOTSTRAP_ResourceGroup.ToLower() + \"batch\"\r\n",
"$Env:BOOTSTRAP_VNET = $Env:BOOTSTRAP_ResourceGroup + \"Vnet\""
],
"metadata": {
"azdata_cell_guid": "417edc0e-1107-4a27-a4cf-e921f79b3f6a",
"tags": []
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"source": [
@@ -105,9 +106,9 @@
" $subscriptions = az account list -o JSON | ConvertFrom-Json # getting subscriptions for the user to use in gridview\r\n",
" }\r\n",
"\r\n",
" if(![string]::IsNullOrWhiteSpace($Env:BOOTSTRAP_Subscription)) #If there is a subscription specified by user in the variables section\r\n",
" if(![string]::IsNullOrWhiteSpace($AdpSubscription)) #If there is a subscription specified by user in the variables section\r\n",
" {\r\n",
" $specified_Subscription= az account show --subscription $Env:BOOTSTRAP_Subscription -o json |ConvertFrom-Json \r\n",
" $specified_Subscription= az account show --subscription $AdpSubscription -o json |ConvertFrom-Json \r\n",
" if (!$specified_Subscription) #if specified subscription is not valid\r\n",
" { \r\n",
" $currentUser= az ad signed-in-user show --query \"{displayName:displayName,UPN:userPrincipalName}\" -o json|ConvertFrom-Json # get current logged in user infomration\r\n",
@@ -123,8 +124,8 @@
" $selectedSubscription = $subscriptions | Select-Object -Property Name, Id | Out-GridView -PassThru\r\n",
" $SubscriptionId = $selectedSubscription.Id\r\n",
" $Subscription = $selectedSubscription.Name \r\n",
" $Env:BOOTSTRAP_Subscription = $subscription \r\n",
" Write-Output \"Using subscription... '$Env:BOOTSTRAP_Subscription' ... '$SubscriptionId'\" \r\n",
" $AdpSubscription = $subscription \r\n",
" Write-Output \"Using subscription... '$AdpSubscription' ... '$SubscriptionId'\" \r\n",
" } \r\n",
"}\r\n",
"\r\n",
@@ -369,8 +370,8 @@
{
"cell_type": "code",
"source": [
"Verify-ADPResources -Subscription $Env:BOOTSTRAP_Subscription -ADPResourceGroupName $Env:BOOTSTRAP_ResourceGroup `\r\n",
" -BatchAccountName $Env:BOOTSTRAP_BATCH -FunctionName $Env:BOOTSTRAP_FUNC -VNetName $Env:BOOTSTRAP_VNET "
"Verify-ADPResources -Subscription $AdpSubscription -ADPResourceGroupName $AdpResourceGroup `\r\n",
" -BatchAccountName $AdpBatch -FunctionName $AdpFunc -VNetName $AdpVNET "
],
"metadata": {
"azdata_cell_guid": "8185f2ea-d368-42c5-9246-bc1871affc63"
@@ -391,7 +392,7 @@
{
"cell_type": "code",
"source": [
"Provision-FuncRBAC -FunctionName $Env:BOOTSTRAP_FUNC -ScopeRGName $SourceResourceGroupName -ResourceGroupName $Env:BOOTSTRAP_ResourceGroup -Subscription $Env:BOOTSTRAP_Subscription"
"Provision-FuncRBAC -FunctionName $AdpFunc -ScopeRGName $SourceResourceGroup -ResourceGroupName $AdpResourceGroup -Subscription $AdpSubscription"
],
"metadata": {
"azdata_cell_guid": "7678701e-ec40-43d9-baff-fd1cdabba1cd"
@@ -413,7 +414,7 @@
{
"cell_type": "code",
"source": [
"$sqlServer = az sql server show --name $LogicalSQLServerName --resource-group $SourceResourceGroupName --subscription $Env:BOOTSTRAP_Subscription -o JSON | ConvertFrom-JSON\r\n",
"$sqlServer = az sql server show --name $LogicalSQLServerName --resource-group $SourceResourceGroup --subscription $AdpSubscription -o JSON | ConvertFrom-JSON\r\n",
"if ($sqlServer)\r\n",
"{\r\n",
" Write-Host \"Source SQL Server: \" $sqlServer.name\r\n",
@@ -426,7 +427,7 @@
" Write-Host \"ERROR: Source server is not in Ready state. Current state is: \" $sqlServer.state\r\n",
" }\r\n",
"\r\n",
" $sqlAzureAdmin = az sql server ad-admin list --server $LogicalSQLServerName --resource-group $SourceResourceGroupName --subscription $Env:BOOTSTRAP_Subscription -o JSON | ConvertFrom-JSON\r\n",
" $sqlAzureAdmin = az sql server ad-admin list --server $LogicalSQLServerName --resource-group $SourceResourceGroup --subscription $AdpSubscription -o JSON | ConvertFrom-JSON\r\n",
" if ($sqlAzureAdmin)\r\n",
" {\r\n",
" Write-Host \"Azure AD admin set to\" $sqlAzureAdmin.login\r\n",
@@ -442,8 +443,8 @@
"{\r\n",
" Write-Host \"ERROR: Source server \" $sqlServer.name \"not found or current account lacks access to resource.\"\r\n",
" Write-Host \"Validate input settings:\"\r\n",
" Write-Host \"Resource group: \" $SourceResourceGroupName\r\n",
" Write-Host \"Subscription: \" $Env:BOOTSTRAP_Subscription\r\n",
" Write-Host \"Resource group: \" $SourceResourceGroup\r\n",
" Write-Host \"Subscription: \" $AdpSubscription\r\n",
"}"
],
"metadata": {
@@ -464,8 +465,8 @@
{
"cell_type": "code",
"source": [
"$InputForExportFunction = Prepare-InputForExportFunction -Subscription $Env:BOOTSTRAP_Subscription -ADPResourceGroupName $Env:BOOTSTRAP_ResourceGroup `\r\n",
" -BatchAccountName $Env:BOOTSTRAP_BATCH -FunctionName $Env:BOOTSTRAP_FUNC -VNetName $Env:BOOTSTRAP_VNET -SourceRGName $SourceResourceGroupName `\r\n",
"$InputForExportFunction = Prepare-InputForExportFunction -Subscription $AdpSubscription -ADPResourceGroupName $AdpResourceGroup `\r\n",
" -BatchAccountName $AdpBatch -FunctionName $AdpFunc -VNetName $AdpVNET -SourceRGName $SourceResourceGroup `\r\n",
" -SqlServerName $LogicalSQLServerName -StorageAccountName $StorageAccount\r\n",
"Write-Host \"Setting parameter variables for Export Function Call...\"\r\n",
"$InputForExportFunction.Header\r\n",
@@ -528,7 +529,7 @@
" Write-Host \"`tCreated Export Batch Job ID: \" $batchJobId\r\n",
" Write-Host \"`tExport container URL: \" $containerUrl\r\n",
"\r\n",
" $azBatchLogin = az batch account login --name $Env:BOOTSTRAP_BATCH --resource-group $Env:BOOTSTRAP_ResourceGroup -o JSON | ConvertFrom-Json\r\n",
" $azBatchLogin = az batch account login --name $AdpBatch --resource-group $AdpResourceGroup -o JSON | ConvertFrom-Json\r\n",
" $jobStatus = az batch job show --job-id $batchJobID -o JSON | ConvertFrom-Json\r\n",
" Write-Host \"Export Job running on Pool: \" $jobStatus.poolInfo.poolId\r\n",
" Write-Host \"`tExport Request Status: \" $jobStatus.state\r\n",

View File

@@ -63,8 +63,8 @@
"cell_type": "code",
"source": [
"# ADP Resource \r\n",
"$Env:BOOTSTRAP_Subscription = \"\" # Azure Subscription ID/Name # The bacpac files and ADP Resources are assumed to be in the same subscription\r\n",
"$Env:BOOTSTRAP_ResourceGroup = \"\" # Azure Resource Group which contains the ADP Resources\r\n",
"$AdpSubscription = \"\" # Azure Subscription ID/Name # The bacpac files and ADP Resources are assumed to be in the same subscription\r\n",
"$AdpResourceGroup = \"\" # Azure Resource Group which contains the ADP Resources\r\n",
"\r\n",
"# SQL Server \r\n",
"$TargetResourceGroupName = \"\" # Azure ResourceGroup into which the sql server backup needs to be restored\r\n",
@@ -74,9 +74,9 @@
"$LSqlServerPassword = \"\"\r\n",
"\r\n",
"# Set Variables for ADP Resources\r\n",
"$Env:BOOTSTRAP_FUNC = $Env:BOOTSTRAP_ResourceGroup + \"Control\" \r\n",
"$Env:BOOTSTRAP_BATCH = $Env:BOOTSTRAP_ResourceGroup.ToLower() + \"batch\"\r\n",
"$Env:BOOTSTRAP_VNET = $Env:BOOTSTRAP_ResourceGroup + \"Vnet\""
"$AdpFunc = $AdpResourceGroup + \"Control\" \r\n",
"$AdpBatch = $AdpResourceGroup.ToLower() + \"batch\"\r\n",
"$AdpVNET = $AdpResourceGroup + \"Vnet\""
],
"metadata": {
"azdata_cell_guid": "01888595-0d1c-445b-ba85-dd12caa30192",
@@ -112,9 +112,9 @@
" $subscriptions = az account list -o JSON | ConvertFrom-Json # getting subscriptions for the user to use in gridview\r\n",
" }\r\n",
"\r\n",
" if(![string]::IsNullOrWhiteSpace($Env:BOOTSTRAP_Subscription)) #If there is a subscription specified by user in the variables section\r\n",
" if(![string]::IsNullOrWhiteSpace($AdpSubscription)) #If there is a subscription specified by user in the variables section\r\n",
" {\r\n",
" $specified_Subscription= az account show --subscription $Env:BOOTSTRAP_Subscription -o json |ConvertFrom-Json \r\n",
" $specified_Subscription= az account show --subscription $AdpSubscription -o json |ConvertFrom-Json \r\n",
" if (!$specified_Subscription) #if specified subscription is not valid\r\n",
" { \r\n",
" $currentUser= az ad signed-in-user show --query \"{displayName:displayName,UPN:userPrincipalName}\" -o json|ConvertFrom-Json # get current logged in user infomration\r\n",
@@ -130,8 +130,8 @@
" $selectedSubscription = $subscriptions | Select-Object -Property Name, Id | Out-GridView -PassThru\r\n",
" $SubscriptionId = $selectedSubscription.Id\r\n",
" $Subscription = $selectedSubscription.Name \r\n",
" $Env:BOOTSTRAP_Subscription = $subscription \r\n",
" Write-Output \"Using subscription... '$Env:BOOTSTRAP_Subscription' ... '$SubscriptionId'\" \r\n",
" $AdpSubscription = $subscription \r\n",
" Write-Output \"Using subscription... '$AdpSubscription' ... '$SubscriptionId'\" \r\n",
" } \r\n",
"}\r\n",
"\r\n",
@@ -381,8 +381,8 @@
{
"cell_type": "code",
"source": [
"Verify-ADPResources -Subscription $Env:BOOTSTRAP_Subscription -ADPResourceGroupName $Env:BOOTSTRAP_ResourceGroup `\r\n",
" -BatchAccountName $Env:BOOTSTRAP_BATCH -FunctionName $Env:BOOTSTRAP_FUNC -VNetName $Env:BOOTSTRAP_VNET "
"Verify-ADPResources -Subscription $AdpSubscription -ADPResourceGroupName $AdpResourceGroup `\r\n",
" -BatchAccountName $AdpBatch -FunctionName $AdpFunc -VNetName $AdpVNET "
],
"metadata": {
"azdata_cell_guid": "e89f6eb9-fcbc-4b7d-bcd1-37f1eb52cc02",
@@ -406,7 +406,7 @@
{
"cell_type": "code",
"source": [
"Provision-FuncRBAC -FunctionName $Env:BOOTSTRAP_FUNC -ScopeRGName $TargetResourceGroupName -ResourceGroupName $Env:BOOTSTRAP_ResourceGroup -Subscription $Env:BOOTSTRAP_Subscription"
"Provision-FuncRBAC -FunctionName $AdpFunc -ScopeRGName $TargetResourceGroupName -ResourceGroupName $AdpResourceGroup -Subscription $AdpSubscription"
],
"metadata": {
"azdata_cell_guid": "c374e57c-51ec-4a3f-9966-1e50cefc8510"
@@ -426,9 +426,9 @@
{
"cell_type": "code",
"source": [
"$InputForImportFunction = Prepare-InputForImportFunction -Subscription $Env:BOOTSTRAP_Subscription -ADPResourceGroupName $Env:BOOTSTRAP_ResourceGroup `\r\n",
" -BatchAccountName $Env:BOOTSTRAP_BATCH -FunctionName $Env:BOOTSTRAP_FUNC -TargetRGName $TargetResourceGroupName `\r\n",
" -VNetName $Env:BOOTSTRAP_VNET -BackupFiles_StorageAccount $StorageAccountName -BackupFiles_ContainerName $ContainerName `\r\n",
"$InputForImportFunction = Prepare-InputForImportFunction -Subscription $AdpSubscription -ADPResourceGroupName $AdpResourceGroup `\r\n",
" -BatchAccountName $AdpBatch -FunctionName $AdpFunc -TargetRGName $TargetResourceGroupName `\r\n",
" -VNetName $AdpVNET -BackupFiles_StorageAccount $StorageAccountName -BackupFiles_ContainerName $ContainerName `\r\n",
" -SqlServerName $LogicalSQLServerName -SqlServerPassword $LSqlServerpassword\r\n",
"Write-Host \"Setting parameter variables for Import Function Call...\"\r\n",
"$InputForImportFunction.Header\r\n",
@@ -495,7 +495,7 @@
" $containerUrl = $outputParams.Item2[3]\r\n",
"\r\n",
" Write-Host \"`tCreated Import Batch Job ID: \" $batchJobId\r\n",
" $azBatchLogin = az batch account login --name $Env:BOOTSTRAP_BATCH --resource-group $Env:BOOTSTRAP_ResourceGroup -o JSON | ConvertFrom-Json\r\n",
" $azBatchLogin = az batch account login --name $AdpBatch --resource-group $AdpResourceGroup -o JSON | ConvertFrom-Json\r\n",
" $jobStatus = az batch job show --job-id $batchJobID -o JSON | ConvertFrom-Json\r\n",
" Write-Host \"Import Job running on Pool: \" $jobStatus.poolInfo.poolId\r\n",
" Write-Host \"`Import Request Status: \" $jobStatus.state\r\n",

View File

@@ -1,15 +1,20 @@
# Data Portability
[Home](../readme.md)
Notebooks in this chapter perform a data migration using a custom Azure function that can be deployed to an Azure subscription. It enables [Azure Batch](https://azure.microsoft.com/en-us/services/batch) computing of a complex SQL Server migration to and from a single Resource Group. Azure Batch is a process that runs large-scale parallel and high-performance computing (HPC) batch jobs efficiently in Azure. This greatly reduces the processing required locally which should prevent long execution times, timeouts and retries. Importing and exporting data to and from Azure is supported for multiple SQL database instances. Data is imported and exported to and from standard SQL backup formats (*.bacpac) which "encapsulates the database schema as well as the data stored in the database" ([Microsoft Docs](https://docs.microsoft.com/en-us/sql/relational-databases/data-tier-applications/data-tier-applications)).
## Notebooks in this Chapter
- [Azure Data Portability Setup](bootstrap.ipynb) - Configure and install a custom Azure function to migrate data to and from Azure
- [Azure Data Portability Setup](setup-adp.ipynb) - Configure and install a custom Azure function to migrate data to and from Azure <br/>
<img width="25%" src="VisualBootstrapperNB.PNG"/>
- [Export Sql Server](export-sql-server.ipynb) - from SQL Azure to a standard SQL backup format
- [Import Sql Server](import-sql-server.ipynb) - from SQL backup format to Azure
The Notebooks in this chapter perform a data migration using a custom Azure function that can be deployed to an Azure subscription. It enables [Azure Batch](https://azure.microsoft.com/en-us/services/batch) computing of a complex SQL Server migration to and from a single Resource Group. Azure Batch is a process that runs large-scale parallel and high-performance computing (HPC) batch jobs efficiently in Azure. This greatly reduces the processing required locally which should prevent long execution times, timeouts and retries. Importing and exporting data to and from Azure is supported for multiple SQL database instances. Data is imported and exported to and from standard SQL backup formats (*.bacpac) which "encapsulates the database schema as well as the data stored in the database" ([Microsoft Docs](https://docs.microsoft.com/en-us/sql/relational-databases/data-tier-applications/data-tier-applications)).
## Steps
1. The Azure function must first be deployed using the setup notebook
2. Open the notebook for the desired migration path
2. Open the notebook for the desired migration path (import or export)
3. Configure and execute notebook
4. Monitor progress with periodic notebook queries
5. Verify data has been imported/exported by reviewing the storage account for the migrated Resource Group

View File

@@ -55,22 +55,22 @@
"cell_type": "code",
"source": [
"# Setup client environment variables that the rest of the notebook will use\r\n",
"$Env:BOOTSTRAP_ResourceGroup = \"\" # Target Resource Group to bootstrap with ADP components - A new one will be created if the specified Resource Group doesn't exist\r\n",
"$Env:BOOTSTRAP_RG_REGION = \"eastus\" # Region/Location of the resource group to be bootstrapped\r\n",
"$AdpResourceGroup = \"\" # Target Resource Group to bootstrap with ADP components - A new one will be created if the specified Resource Group doesn't exist\r\n",
"$AdpRegion = \"eastus\" # Region/Location of the resource group to be bootstrapped\r\n",
"\r\n",
"# Derived settings\r\n",
"$Env:BOOTSTRAP_Subscription = \"\" # Target Azure Subscription Name or ID to bootstrap data portability resources\r\n",
"$Env:BOOTSTRAP_FUNC = $Env:BOOTSTRAP_ResourceGroup + \"Control\"\r\n",
"$Env:BOOTSTRAP_STORAGE = $Env:BOOTSTRAP_ResourceGroup.ToLower() + \"storage\"\r\n",
"$Env:BOOTSTRAP_BATCH = $Env:BOOTSTRAP_ResourceGroup.ToLower() + \"batch\"\r\n",
"$Env:BOOTSTRAP_VNET = $Env:BOOTSTRAP_ResourceGroup + \"VNet\"\r\n",
"$AdpSubscription = \"\" # Target Azure Subscription Name or ID to bootstrap data portability resources\r\n",
"$AdpFunc = $AdpResourceGroup + \"Control\"\r\n",
"$AdpStorage = $AdpResourceGroup.ToLower() + \"storage\"\r\n",
"$AdpBatch = $AdpResourceGroup.ToLower() + \"batch\"\r\n",
"$AdpVNET = $AdpResourceGroup + \"VNet\"\r\n",
"\r\n",
"# Bootstrapper URLs - Update with the recommended toolkit version and build\r\n",
"$BaseToolkitUrl = \"https://hybridtoolkit.blob.core.windows.net/components\"\r\n",
"$ReleaseVersion = \"0.13\"\r\n",
"$BuildNumber = \"74938\"\r\n",
"$Env:BOOTSTRAP_URL_FUNC = \"$BaseToolkitUrl/$ReleaseVersion/ADPControl-$BuildNumber.zip\"\r\n",
"$Env:BOOTSTRAP_URL_WRAP = \"$BaseToolkitUrl/$ReleaseVersion/BatchWrapper-$BuildNumber.zip\"\r\n",
"$AdpDownloadUrl = \"$BaseToolkitUrl/$ReleaseVersion/ADPControl-$BuildNumber.zip\"\r\n",
"$AdpWrapperUrl = \"$BaseToolkitUrl/$ReleaseVersion/BatchWrapper-$BuildNumber.zip\"\r\n",
"\r\n",
"Write-Output \"Setting the Environment:\"\r\n",
"Get-ChildItem Env: | Where-Object Name -Match \"BOOTSTRAP\""
@@ -121,9 +121,9 @@
" $subscriptions = az account list -o JSON | ConvertFrom-Json # getting subscriptions for the user to use in gridview\r\n",
" }\r\n",
"\r\n",
" if(![string]::IsNullOrWhiteSpace($Env:BOOTSTRAP_Subscription)) #If there is a subscription specified by user in the variables section\r\n",
" if(![string]::IsNullOrWhiteSpace($AdpSubscription)) #If there is a subscription specified by user in the variables section\r\n",
" {\r\n",
" $specified_Subscription= az account show --subscription $Env:BOOTSTRAP_Subscription -o json |ConvertFrom-Json \r\n",
" $specified_Subscription= az account show --subscription $AdpSubscription -o json |ConvertFrom-Json \r\n",
" if (!$specified_Subscription) #if specified subscription is not valid\r\n",
" { \r\n",
" $currentUser= az ad signed-in-user show --query \"{displayName:displayName,UPN:userPrincipalName}\" -o json|ConvertFrom-Json # get current logged in user infomration\r\n",
@@ -139,8 +139,8 @@
" $selectedSubscription = $subscriptions | Select-Object -Property Name, Id | Out-GridView -PassThru\r\n",
" $SubscriptionId = $selectedSubscription.Id\r\n",
" $Subscription = $selectedSubscription.Name \r\n",
" $Env:BOOTSTRAP_Subscription = $subscription \r\n",
" Write-Output \"Using subscription... '$Env:BOOTSTRAP_Subscription' ... '$SubscriptionId'\" \r\n",
" $AdpSubscription = $subscription \r\n",
" Write-Output \"Using subscription... '$AdpSubscription' ... '$SubscriptionId'\" \r\n",
" } \r\n",
"}\r\n",
"\r\n",
@@ -207,8 +207,8 @@
" else { \r\n",
" #VNet or defaut subnet not found under specified resource group. Create new VNet with default Subnet /Add default subnet to existing VNet\r\n",
" Write-Output \"Creating new Virtual network with default Subnet ID ... \"\r\n",
" $newVNet = az network vnet create --name \"$Env:BOOTSTRAP_VNET\" --resource-group $Env:BOOTSTRAP_ResourceGroup --subscription $Env:BOOTSTRAP_Subscription --subnet-name $SubNetName -o JSON |ConvertFrom-Json #vnet create/Update command: Bug: In this command, the output variable is not getting converted to PS objects.\r\n",
" $newVNet = az network vnet subnet show -g $Env:BOOTSTRAP_ResourceGroup --vnet-name $Env:BOOTSTRAP_VNET -n $SubNetName --subscription $Env:BOOTSTRAP_Subscription -o JSON |ConvertFrom-Json # added this line due to above bug\r\n",
" $newVNet = az network vnet create --name \"$AdpVNET\" --resource-group $AdpResourceGroup --subscription $AdpSubscription --subnet-name $SubNetName -o JSON |ConvertFrom-Json #vnet create/Update command: Bug: In this command, the output variable is not getting converted to PS objects.\r\n",
" $newVNet = az network vnet subnet show -g $AdpResourceGroup --vnet-name $AdpVNET -n $SubNetName --subscription $AdpSubscription -o JSON |ConvertFrom-Json # added this line due to above bug\r\n",
" Write-Output \"Created VNet with default Subnet - ID: '$($newVNet.id)'\"\r\n",
" }\r\n",
"}\r\n",
@@ -508,7 +508,7 @@
{
"cell_type": "code",
"source": [
"Bootstrap-AzResourceGroup -ResourceGroupName $Env:BOOTSTRAP_ResourceGroup -ResourceGroupLocation $Env:BOOTSTRAP_RG_REGION -Subscription $Env:BOOTSTRAP_Subscription"
"Bootstrap-AzResourceGroup -ResourceGroupName $AdpResourceGroup -ResourceGroupLocation $AdpRegion -Subscription $AdpSubscription"
],
"metadata": {
"azdata_cell_guid": "9beb8d22-4560-4c7e-917b-5a3c0d58e1a2",
@@ -533,7 +533,7 @@
{
"cell_type": "code",
"source": [
"Bootstrap-AzVirtualNetwork -VNetName $Env:BOOTSTRAP_VNET -ResourceGroupName $Env:BOOTSTRAP_ResourceGroup -Subscription $Env:BOOTSTRAP_Subscription"
"Bootstrap-AzVirtualNetwork -VNetName $AdpVNET -ResourceGroupName $AdpResourceGroup -Subscription $AdpSubscription"
],
"metadata": {
"azdata_cell_guid": "d014a6a6-57ff-4de7-8210-b3360bf34daa"
@@ -555,7 +555,7 @@
{
"cell_type": "code",
"source": [
"Bootstrap-AzStorageAccount -StorageAccountName $Env:BOOTSTRAP_STORAGE -ResourceGroupName $Env:BOOTSTRAP_ResourceGroup -Subscription $Env:BOOTSTRAP_Subscription"
"Bootstrap-AzStorageAccount -StorageAccountName $AdpStorage -ResourceGroupName $AdpResourceGroup -Subscription $AdpSubscription"
],
"metadata": {
"azdata_cell_guid": "290498ee-3f31-4395-adab-a5fa93d28c80",
@@ -580,8 +580,8 @@
{
"cell_type": "code",
"source": [
"Bootstrap-AzFunctionApp -FunctionName $Env:BOOTSTRAP_FUNC -StorageAccountName $Env:BOOTSTRAP_STORAGE -FunctionAppPackageURL $Env:BOOTSTRAP_URL_FUNC `\r\n",
" -ConsumptionPlanLocation $Env:BOOTSTRAP_RG_REGION -ResourceGroupName $Env:BOOTSTRAP_ResourceGroup -Subscription $Env:BOOTSTRAP_Subscription"
"Bootstrap-AzFunctionApp -FunctionName $AdpFunc -StorageAccountName $AdpStorage -FunctionAppPackageURL $AdpDownloadUrl `\r\n",
" -ConsumptionPlanLocation $AdpRegion -ResourceGroupName $AdpResourceGroup -Subscription $AdpSubscription"
],
"metadata": {
"azdata_cell_guid": "6fc2b5ec-c16f-4eb7-b2f9-c8c680d9a2df",
@@ -605,8 +605,8 @@
{
"cell_type": "code",
"source": [
"Bootstrap-AzBatchAccount -BatchAccountName $Env:BOOTSTRAP_BATCH -StorageAccountName $Env:BOOTSTRAP_STORAGE -BatchAccountLocation $Env:BOOTSTRAP_RG_REGION `\r\n",
" -ApplicationPackageURL $Env:BOOTSTRAP_URL_WRAP -ResourceGroupName $Env:BOOTSTRAP_ResourceGroup -Subscription $Env:BOOTSTRAP_Subscription"
"Bootstrap-AzBatchAccount -BatchAccountName $AdpBatch -StorageAccountName $AdpStorage -BatchAccountLocation $AdpRegion `\r\n",
" -ApplicationPackageURL $AdpWrapperUrl -ResourceGroupName $AdpResourceGroup -Subscription $AdpSubscription"
],
"metadata": {
"azdata_cell_guid": "489733c4-1162-479b-82b4-b0c18954b25b",
@@ -628,7 +628,7 @@
{
"cell_type": "code",
"source": [
"Bootstrap-FuncRBAC -AzFunctionName $Env:BOOTSTRAP_FUNC -ResourceGroupName $Env:BOOTSTRAP_ResourceGroup -Subscription $Env:BOOTSTRAP_Subscription"
"Bootstrap-FuncRBAC -AzFunctionName $AdpFunc -ResourceGroupName $AdpResourceGroup -Subscription $AdpSubscription"
],
"metadata": {
"azdata_cell_guid": "75882d3a-2004-4304-ab8f-e5146e14500c",

View File

@@ -1,4 +1,6 @@
# Glossary
[Home](readme.md)
A list of terms and their definitions can be found below
* **ADS** - *Azure Data Studio* is a desktop tool for managing Azure Data resources in the cloud, on-premises, or hybrid environments.
@@ -31,4 +33,5 @@ A list of terms and their definitions can be found below
* **SQL Assessment API** - evaluates a SQL instance configuration for best practices
* **SQL Virtual Machine** - an IaaS Azure offer that provisions and manages virtual machine with SQL Server installed
* **SQL Managed Instance** - a PaaS Azure offer for SQL Server that is ran on Azure infrastructure. Microsoft will manage the complexities of the infrastructure for the user
* **SMO** - SQL Management Objects are "objects designed for programmatic management of Microsoft SQL Server" ([Microsoft](https://docs.microsoft.com/en-us/sql/relational-databases/server-management-objects-smo/overview-smo))
* **VPN** - a *virtual private network* is a collection of computing resources that organizes and extends a private network configuration over the public Internet, normally using some kind of encryption for security and privacy.

View File

@@ -1,39 +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": "markdown",
"source": [
"Add Azure Passive Secondary Replica\n",
"============================================\n",
"\n",
"Description\n",
"-----------\n",
"\n",
"Notebook to walkthrough extending an on-premises Availability Group with an Azure Passive Secondary Replica."
],
"metadata": {
"azdata_cell_guid": "a7c75090-5d5f-4a1b-8712-461a0921f4ad"
}
}
]
}

View File

@@ -1,9 +1,8 @@
# High Availability and Disaster Recovery
[Home](../readme.md)
**Coming soon**: Notebooks to help with HADR tasks in a Hybrid Cloud environment.
Notebooks to help with HADR tasks in a Hybrid Cloud environment.
## Notebooks in this Chapter
- [Backup Database to Blob Storage](backup-to-blob.ipynb)
- [Add Azure Passive Secondary Replica](add-passive-secondary.ipynb)

View File

@@ -1,12 +1,13 @@
# Networking
[Home](../readme.md)
This chapter contains notebooks to configure and make a secure network connection in an Azure hybrid cloud environment.
<img width="50%" src="https://docs.microsoft.com/en-us/azure/vpn-gateway/media/point-to-site-about/p2s.png">
## Notebooks in this Chapter
- [Download VPN Client Certificate](download-VpnClient.ipynb) - Used to install certificates that encrypt communication between on-site and Azure services
- [Create Point-to-Site VPN](p2svnet-creation.ipynb) - Enables secure **Point-to-Site** (P2S) communication between a virtual private network in Azure and local resources. P2S is used by individuals and small groups for remote connectivity. A Point-to-Site (P2S) VPN gateway connection lets you create a secure connection to your VPN from an individual client computer. A P2S connection is established by starting it from the client computer. This solution is useful for telecommuters who want to connect to Azure VNets from a remote location, such as from home or a conference. P2S VPN is also a useful solution to use instead of S2S VPN when you have only a few clients that need to connect to a virtual network.
- [Create Site-to-Site VPN](s2svnet-creation.ipynb) - **Site-to-site** (S2S) is normally used by organizations that want greater control between on-premise and cloud resources using a VPN gateway. A S2S VPN gateway connection is used to connect your on-premises network to an Azure virtual network over an IPsec/IKE (IKEv1 or IKEv2) VPN tunnel. This type of connection requires a VPN device located on-premises that has an externally facing public IP address assigned to it. For more information about VPN gateways, see [About VPN gateway](https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-about-vpngateways) and [Create and manage S2S VPN connections using PowerShell](https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-tutorial-vpnconnection-powershell "https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-tutorial-vpnconnection-powershell"). **NOTE:** *May require the help of a Network Administrator or similar role to setup a secure Gateway*.
This chapter contains notebooks to configure and make a secure network connection in an Azure hybrid cloud environment.
<img width="50%" src="https://docs.microsoft.com/en-us/azure/vpn-gateway/media/point-to-site-about/p2s.png">
- [Create Site-to-Site VPN](s2svnet-creation.ipynb) - **Site-to-site** (S2S) is normally used by organizations that want greater control between on-premise and cloud resources using a VPN gateway. A S2S VPN gateway connection is used to connect your on-premises network to an Azure virtual network over an IPsec/IKE (IKEv1 or IKEv2) VPN tunnel. This type of connection requires a VPN device located on-premises that has an externally facing public IP address assigned to it. For more information about VPN gateways, see [About VPN gateway](https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-about-vpngateways) and [Create and manage S2S VPN connections using PowerShell](https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-tutorial-vpnconnection-powershell "https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-tutorial-vpnconnection-powershell"). **NOTE:** *May require the help of a Network Administrator or similar role to setup a secure Gateway*.

View File

@@ -1,39 +0,0 @@
{
"metadata": {
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"language_info": {
"name": "python",
"version": "3.7.8",
"mimetype": "text/x-python",
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"pygments_lexer": "ipython3",
"nbconvert_exporter": "python",
"file_extension": ".py"
}
},
"nbformat_minor": 2,
"nbformat": 4,
"cells": [
{
"cell_type": "markdown",
"source": [
"Migrate a Database to a Azure SQL Managed Instance\n",
"=============================================\n",
"\n",
"Description\n",
"-----\n",
"\n",
"Copies the database from an on-premises SQL instance to an Azure SQL Managed Instance."
],
"metadata": {
"azdata_cell_guid": "5353c044-9920-478b-b1f8-e98119b73a21"
}
}
]
}

View File

@@ -51,15 +51,14 @@
{
"cell_type": "code",
"source": [
"$sourceServerName = 'sqltools2016-3'\r\n",
"$sourceLogin = 'migtest'\r\n",
"$sourceServerName = '<server_name>'\r\n",
"$sourceLogin = '<user_name>'\r\n",
"\r\n",
"## TEMP - REMOVE BEFORE PUSHING CHANGES\r\n",
"$env:SQLMIG_SourcePassword = 'Yukon900'\r\n",
"$env:SQLMIG_SourcePassword = '<user_pass>'\r\n",
"\r\n",
"## PowerShell Environment \r\n",
"$sourceLoginPassword = ConvertTo-SecureString $env:SQLMIG_SourcePassword -AsPlaintext -Force\r\n",
"$sourceCredential = New-Object System.Management.Automation.PSCredential ('migtest', $sourceLoginPassword)\r\n",
"$sourceCredential = New-Object System.Management.Automation.PSCredential ('<user_name>', $sourceLoginPassword)\r\n",
"$sourceTest = Test-DbaConnection -SqlInstance $sourceServerName -SqlCredential $sourceCredential\r\n",
"$sourceTest\r\n",
"$sourceConnection = Connect-DbaInstance -SqlInstance $sourceServerName -SqlCredential $sourceCredential"
@@ -98,11 +97,11 @@
"$targetLogin = 'cloudsa'\r\n",
"\r\n",
"## TEMP - REMOVE BEFORE PUSHING CHANGES\r\n",
"$env:SQLMIG_TargetPassword = 'Yukon900Yukon900'\r\n",
"$env:SQLMIG_TargetPassword = '<user_pass>'\r\n",
"\r\n",
"## PowerShell Environment \r\n",
"$targetLoginPassword = ConvertTo-SecureString $env:SQLMIG_TargetPassword -AsPlaintext -Force\r\n",
"$targetCredential = New-Object System.Management.Automation.PSCredential ('migtest', $targetLoginPassword)\r\n",
"$targetCredential = New-Object System.Management.Automation.PSCredential ('<user_name>', $targetLoginPassword)\r\n",
"$targetTest = Test-DbaConnection -SqlInstance $targetServerName -SqlCredential $targetCredential\r\n",
"$targetTest\r\n",
"$targetConnection = Connect-DbaInstance -SqlInstance $targetServerName -SqlCredential $targetCredential"
@@ -268,4 +267,4 @@
"execution_count": null
}
]
}
}

View File

@@ -1,34 +0,0 @@
{
"metadata": {
"kernelspec": {
"name": "powershell",
"display_name": "PowerShell"
},
"language_info": {
"name": "powershell",
"codemirror_mode": "shell",
"mimetype": "text/x-sh",
"file_extension": ".ps1"
}
},
"nbformat_minor": 2,
"nbformat": 4,
"cells": [
{
"cell_type": "markdown",
"source": [
"Migrate SQL Server Instance to Azure SQL Managed Instance\n",
"=============================================\n",
"\n",
"Description\n",
"-----\n",
"\n",
"clone the configuration and data of a sql instance into a managed instance\n",
""
],
"metadata": {
"azdata_cell_guid": "43600853-57b3-4e60-a2a9-a28fb82af386"
}
}
]
}

View File

@@ -8,8 +8,5 @@ This chapter contains a set of notebooks useful for doing offline migration of d
- [Migrate Database to Azure SQL VM](db-to-VM.ipynb)
- [Migrate Instance to Azure SQL MI](instance-to-MI.ipynb)
- [Migrate Database to Azure SQL MI](db-to-MI.ipynb)
- [Migrate Database to Azure SQL DB](db-to-SQLDB.ipynb)

View File

@@ -126,7 +126,13 @@
"SQL Assessment API is part of the SQL Server Management Objects (SMO) and can be used with the SQL Server PowerShell module. Because installing the modules may require a local Administrator account's permission, it cannot be done automatically with this Notebook. The **Assessments** Notebooks require the following:\n",
"\n",
"- [Install SMO](https://docs.microsoft.com/en-us/sql/relational-databases/server-management-objects-smo/installing-smo?view=sql-server-ver15)\n",
"- [Install SQL Server PowerShell module](https://docs.microsoft.com/en-us/sql/powershell/download-sql-server-ps-module?view=sql-server-ver15)"
"- [Install SQL Server PowerShell module](https://docs.microsoft.com/en-us/sql/powershell/download-sql-server-ps-module?view=sql-server-ver15)\n",
"\n",
"## Compatibility Assessment Tool - Data Migration Assistant\n",
"\n",
"The Compatibility Assessment Notebook requires the Data Migration Assistant tool to be installed in order to execute. The installation link would be [Data Migration Assistant download](https://www.microsoft.com/en-us/download/confirmation.aspx?id=53595)\n",
"\n",
"With version 2.1 and above, when installation of Data Migration Assistant is successful, it will install dmacmd.exe in _%ProgramFiles%\\\\Microsoft Data Migration Assistant_ folder."
],
"metadata": {
"azdata_cell_guid": "1b49a7e5-a773-4104-8f88-bd2ea3c806a3"

View File

@@ -1,8 +1,9 @@
# Azure SQL Provisioning
# Provisioning
[Home](../readme.md)
This chapter contains Notebooks that help provision new Azure SQL resources that can be used as migration targets for existing on-premises SQL instances and databases. Use alongside the planning notebooks to use existing resources as the basis for the best type of resource to create and how it should be configured. You can use the notebooks and configure the settings manually or provide a provisioning plan created by the [Create Provisioning Plan](../provisioning/provisioning-plan.ipynb) notebook.
## Notebooks in this Chapter
- [Create Azure SQL Virtual Machine](create-sqlvm.ipynb) - SQL Server on Azure Virtual Machines enables to use full versions of SQL Server in the cloud without having to manage any on-premises hardware. The virtual machine image gallery allows to create a SQL Server VM with the right version, edition, and operating system
- [Create Azure SQL Managed Instance](create-sqlmi.ipynb) - Azure SQL Managed Instance is the intelligent, scalable, cloud database service that combines the broadest SQL Server engine compatibility with all the benefits of a fully managed and evergreen platform as a service. An instance is a copy of the sqlservr.exe executable that runs as an operating system service
- [Create Azure SQL Database](create-sqldb.ipynb) - Azure SQL Database is Microsoft's fully managed cloud relational database service in Microsoft Azure. It shares the same code base as traditional SQL Servers but with Microsoft's Cloud first strategy the newest features of SQL Server are actually released to Azure SQL Database first. Use this notebook when a need is systematic collection of data that stores data in tables
This chapter contains Notebooks that help provision new Azure SQL resources that can be used as migration targets for existing on-premises SQL instances and databases. Use alongside the planning notebooks to use existing resources as the basis for the best type of resource to create and how it should be configured. You can use the notebooks and configure the settings manually or provide a provisioning plan created by the [Create Provisioning Plan](../provisioning/provisioning-plan.ipynb) notebook.
- [Create Azure SQL Database](create-sqldb.ipynb) - Azure SQL Database is Microsoft's fully managed cloud relational database service in Microsoft Azure. It shares the same code base as traditional SQL Servers but with Microsoft's Cloud first strategy the newest features of SQL Server are actually released to Azure SQL Database first. Use this notebook when a need is systematic collection of data that stores data in tables

View File

@@ -1,28 +1,35 @@
# Azure SQL Hybrid Cloud Toolkit
# Welcome to the Azure SQL Hybrid Cloud Toolkit!
## Chapters
* [Prerequisites and Initial Setup](prereqs.ipynb) - Notebook installation of required modules.
The **Azure SQL Hybrid Cloud Toolkit** is a [Jupyter Book](https://jupyterbook.org/intro.html) extension of [Azure Data Studio](https://docs.microsoft.com/en-us/sql/azure-data-studio/download-azure-data-studio) (ADS) designed to help [Azure SQL Database](https://azure.microsoft.com/en-us/services/sql-database/) and ADS users deploy, migrate and configure for a hybrid cloud environment. The toolkit was designed with and intended to be executed within ADS. This is to ensure the best possible experience.
* [Assessments](Assessments/readme.md) - Notebooks that contain examples to determine whether a given database or SQL Server instance is ready to migrate by utilizing SQL Assessments. SQL instances are scanned based on a "best practices" set of rules.
* [Networking](networking/readme.md) - Setup secure Point-to-Site (P2S) or Site-to-Site (S2S) network connectivity to Microsoft Azure using a Virtual Private Network (VPN). This notebook serves as a building block for other notebooks as communicating securely between on-premise and Azure is essential for many tasks.
* [Provisioning](provisioning/readme.md) - Creating and communicating with SQL Resources in Microsoft Azure. Includes common tasks such as creating SQL Virtual Machines or SQL Managed Instances in the cloud.
* [Data Portability](data-portability/readme.md) - Install a custom Azure function to facilitate importing and exporting cloud resources. The solution uses parallel tasks in Azure Batch to perform data storage work. Azure Batch is a process that runs large-scale parallel and high-performance computing jobs efficiently in Azure.
* [High Availability and Disaster Recovery](hadr/readme.md) - Notebooks to leverage Azure SQL for business continuity in a hybrid cloud environment.
* [Offline Migration](offline-migration/readme.md) - Notebooks to perform various migrations.
* [Glossary](glossary.md) - set of defined terms.
* [Appendices](appendices.md) - misc info.
## About
The **Azure SQL Hybrid Cloud Toolkit** is a [Jupyter Book](https://jupyterbook.org/intro.html) extension of [Azure Data Studio](https://docs.microsoft.com/en-us/sql/azure-data-studio/download-azure-data-studio) (ADS) designed to help [Azure SQL Database](https://azure.microsoft.com/en-us/services/sql-database/) and ADS users deploy, migrate and configure for a hybrid cloud environment. The toolkit was designed with and intended to be executed within ADS. This is to ensure the best possible user experience for those without vast knowledge of Azure services while adhering closely to the software _best practices_ standards required by experienced cloud users.
## Goals and Methodology
The toolkit better positions a customer with regards to planning, migrating, and thriving in a hybrid cloud environment by:
* Providing SQL Azure users with reliable free software and content that is well-written and executable
* Providing SQL'zure users with reliable free software and content that is well-written and executable
* Greatly simplifying the integration of Azure Data services into an existing environment
* Positioning Azure to be the natural cloud services choice with a low-friction experience
* Notebooks are executable by a normal user (unless otherwise specificed) on minimal hardware
* Most notebooks require some configuration. If so, the proper configurations should be clearly located towards the top of the notebook or cell, whichever is most appropriate
* Modify the cells to meet the desired requirements
* By design, Notebooks are written to be executed from top-to-bottom. Therefore, each notebook has a specific task to perform and should focus only on that task. It may contain several cells to execute but it will adhere to the one-task per notebook paradigm
**NOTE:** Executing notebooks could potentially create new Azure Resources which may incur charges to the Azure Subscription. Make sure the repercussions of executing any cells are understood.
## Prerequisites and Initial Setup
The notebooks may leverage various modules from Python or Microsoft PowerShell and the OSS community. To execute the notebooks in this toolkit, start with the [Prerequisites and Initial Setup Notebook](Prerequisites/prereqs.ipynb) where all prerequisite modules will be checked and installed if not found in the execution environment.
## Chapters
The toolkit has chapters on network configuration, on-premise SQL Server assessment, resource provisioning, and Azure migration. See below:
* [Networking](networking/readme.md) - Setup secure Point-to-Site (P2S) or Site-to-Site (S2S) network connectivity to Microsoft Azure using a Virtual Private Network (VPN). This notebook serves as a building block for other notebooks as communicating securely between on-premise and Azure is essential for many tasks
* [Assessments](Assessments/readme.md) - Notebooks that contain examples to determine whether a given database or SQL Server instance is ready to migrate by utilizing SQL Assessments. SQL instances are scanned based on a "best practices" set of rules.
* [Provisioning](provisioning/readme.md) - Creating and communicating with SQL Resources in Microsoft Azure. Includes common tasks such as creating SQL Virtual Machines or SQL Managed Instances in the cloud
* [Data Portability](data-portability/readme.md) - Install a custom Azure function to facilitate importing and exporting cloud resources. The solution uses parallel tasks in Azure Batch to perform data storage work. Azure Batch is a process that runs large-scale parallel and high-performance computing jobs efficiently in Azure.
* [High Availability and Disaster Recovery](hadr/readme.md) - Notebooks to leverage Azure SQL for business continuity in a hybrid cloud environment
* [Offline Migration](offline-migration/readme.md) - Notebooks to perform various migrations
**NOTE:** Executing notebooks could potentially create new Azure Resources which may incur charges to the Azure Subscription. Make sure the repercussions of executing any cells are understood.

View File

@@ -4,14 +4,26 @@
"description": "%description%",
"version": "0.1.0",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
"icon": "images/extension.png",
"aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e",
"engines": {
"vscode": "*",
"azdata": "*"
},
"main": "./out/main",
"activationEvents": [
"*"
],
"repository": {
"type": "git",
"url": "https://github.com/Microsoft/azuredatastudio.git"
},
"main": "./out/main",
"extensionDependencies": [
"Microsoft.mssql",
"Microsoft.notebook"
],
"contributes": {
"commands": [
{

View File

@@ -1,5 +1,5 @@
{
"displayName": "Azure SQL Hybrid Cloud Toolkit Jupyter Book Extension",
"displayName": "Azure SQL Hybrid Cloud Toolkit",
"description": "Opens up Azure SQL Hybrid Cloud Toolkit Jupyter Book",
"title.openJupyterBook": "Open Azure SQL Hybrid Cloud Toolkit Jupyter Book",
"title.cloudHybridBooks": "Azure SQL Hybrid Cloud Toolkit",

View File

@@ -1,8 +1,8 @@
{
"extends": "../shared.tsconfig.json",
"compileOnSave": true,
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "./out",
"lib": [
"es6", "es2015.promise"

View File

@@ -369,18 +369,16 @@ export class BdcDashboardOverviewPage extends BdcDashboardPage {
});
endpoints.unshift(...sqlServerMasterEndpoints);
this.endpointsTable.data = endpoints.map(e => {
this.endpointsTable.dataValues = endpoints.map(e => {
const copyValueCell = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({ title: loc.copy }).component();
copyValueCell.iconPath = IconPathHelper.copy;
copyValueCell.onDidClick(() => {
vscode.env.clipboard.writeText(e.endpoint);
vscode.window.showInformationMessage(loc.copiedEndpoint(getEndpointDisplayText(e.name, e.description)));
});
copyValueCell.iconHeight = '14px';
copyValueCell.iconWidth = '14px';
return [getEndpointDisplayText(e.name, e.description),
createEndpointComponent(this.modelView.modelBuilder, e, this.model, hyperlinkedEndpoints.some(he => he === e.name)),
copyValueCell];
return [{ value: getEndpointDisplayText(e.name, e.description) },
{ value: createEndpointComponent(this.modelView.modelBuilder, e, this.model, hyperlinkedEndpoints.some(he => he === e.name)) },
{ value: copyValueCell }];
});
this.endpointsDisplayContainer.removeItem(this.endpointsLoadingComponent);

View File

@@ -35,7 +35,7 @@ export class DacFxTestService implements mssql.IDacFxService {
this.dacfxResult.operationId = extractOperationId;
return Promise.resolve(this.dacfxResult);
}
importDatabaseProject(databaseName: string, targetFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, extractTarget: mssql.ExtractTarget, taskExecutionMode: azdata.TaskExecutionMode): Promise<mssql.DacFxResult> {
createProjectFromDatabase(databaseName: string, targetFilePath: string, applicationName: string, applicationVersion: string, ownerUri: string, extractTarget: mssql.ExtractTarget, taskExecutionMode: azdata.TaskExecutionMode): Promise<mssql.DacFxResult> {
this.dacfxResult.operationId = importOperationId;
return Promise.resolve(this.dacfxResult);
}

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

@@ -0,0 +1,3 @@
<svg width="17" height="12" viewBox="0 0 17 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.8457 3V11C16.8457 11.1406 16.8197 11.2708 16.7676 11.3906C16.7155 11.5104 16.6426 11.6172 16.5488 11.7109C16.4603 11.7995 16.3561 11.8698 16.2363 11.9219C16.1165 11.974 15.9863 12 15.8457 12H1.8457C1.70508 12 1.57487 11.974 1.45508 11.9219C1.33529 11.8698 1.22852 11.7995 1.13477 11.7109C1.04622 11.6172 0.975911 11.5104 0.923828 11.3906C0.871745 11.2708 0.845703 11.1406 0.845703 11V1C0.845703 0.859375 0.871745 0.729167 0.923828 0.609375C0.975911 0.489583 1.04622 0.385417 1.13477 0.296875C1.22852 0.203125 1.33529 0.130208 1.45508 0.078125C1.57487 0.0260417 1.70508 0 1.8457 0H7.5957C7.78841 0 7.9681 0.0364583 8.13477 0.109375C8.30143 0.177083 8.45247 0.270833 8.58789 0.390625C8.72852 0.505208 8.85352 0.638021 8.96289 0.789062C9.07747 0.934896 9.18164 1.08594 9.27539 1.24219C9.3431 1.36198 9.4082 1.46875 9.4707 1.5625C9.53841 1.65625 9.61133 1.73698 9.68945 1.80469C9.77279 1.86719 9.86393 1.91667 9.96289 1.95312C10.0671 1.98438 10.1947 2 10.3457 2H15.8457C15.9863 2 16.1165 2.02604 16.2363 2.07812C16.3561 2.13021 16.4603 2.20312 16.5488 2.29688C16.6426 2.38542 16.7155 2.48958 16.7676 2.60938C16.8197 2.72917 16.8457 2.85938 16.8457 3ZM7.5957 1H1.8457V3H7.5957C7.73633 3 7.85352 2.97656 7.94727 2.92969C8.04622 2.88281 8.13737 2.82552 8.2207 2.75781C8.30924 2.6901 8.39779 2.61719 8.48633 2.53906C8.57487 2.45573 8.67643 2.38281 8.79102 2.32031C8.71289 2.23177 8.62956 2.11458 8.54102 1.96875C8.45768 1.81771 8.36654 1.67188 8.26758 1.53125C8.16862 1.38542 8.06185 1.26042 7.94727 1.15625C7.83789 1.05208 7.7207 1 7.5957 1ZM15.8457 11V3H10.3457C10.054 3 9.81706 3.02604 9.63477 3.07812C9.45768 3.125 9.30664 3.1849 9.18164 3.25781C9.06185 3.33073 8.95768 3.41146 8.86914 3.5C8.7806 3.58854 8.68164 3.66927 8.57227 3.74219C8.4681 3.8151 8.34049 3.8776 8.18945 3.92969C8.03841 3.97656 7.84049 4 7.5957 4H1.8457V11H15.8457Z" fill="#0078D4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -10,10 +10,10 @@
"aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e",
"engines": {
"vscode": "*",
"azdata": ">=1.22.0"
"azdata": ">=1.25.0"
},
"activationEvents": [
"onView:dataworkspace.views.main"
"*"
],
"main": "./out/main",
"repository": {
@@ -30,17 +30,27 @@
"type": "array",
"default": [],
"description": ""
},
"projects.defaultProjectSaveLocation": {
"type": "string",
"description": "%projects.defaultProjectSaveLocation%"
}
}
}
],
"commands": [
{
"command": "projects.addProject",
"title": "%add-project-command%",
"category": "",
"command": "projects.new",
"title": "%new-command%",
"category": "%data-workspace-view-container-name%",
"icon": "$(add)"
},
{
"command": "projects.openExisting",
"title": "%open-existing-command%",
"category": "%data-workspace-view-container-name%",
"icon": "$(folder-opened)"
},
{
"command": "dataworkspace.refresh",
"title": "%refresh-workspace-command%",
@@ -57,18 +67,22 @@
{
"command": "dataworkspace.refresh",
"when": "view == dataworkspace.views.main",
"group": "secondary"
},
{
"command": "projects.new",
"when": "view == dataworkspace.views.main",
"group": "navigation"
},
{
"command": "projects.addProject",
"command": "projects.openExisting",
"when": "view == dataworkspace.views.main",
"group": "navigation"
}
],
"commandPalette": [
{
"command": "projects.addProject",
"when": "false"
"command": "projects.new"
},
{
"command": "dataworkspace.refresh",
@@ -77,6 +91,9 @@
{
"command": "projects.removeProject",
"when": "false"
},
{
"command": "projects.openExisting"
}
],
"view/item/context": [
@@ -102,7 +119,8 @@
"id": "dataworkspace.views.main",
"name": "%main-view-name%",
"contextualTitle": "%data-workspace-view-container-name%",
"icon": "images/data-workspace.svg"
"icon": "images/data-workspace.svg",
"when": "isProjectProviderAvailable"
}
]
},
@@ -111,6 +129,11 @@
"view": "dataworkspace.views.main",
"contents": "%projects-view-no-workspace-content%",
"when": "workbenchState != workspace"
},
{
"view": "dataworkspace.views.main",
"contents": "%projects-view-no-project-content%",
"when": "workbenchState == workspace && isProjectsViewEmpty"
}
]
},

View File

@@ -3,8 +3,11 @@
"extension-description": "Data workspace",
"data-workspace-view-container-name": "Projects",
"main-view-name": "Projects",
"add-project-command": "Add Project",
"new-command": "New",
"refresh-workspace-command": "Refresh",
"remove-project-command": "Remove Project",
"projects-view-no-workspace-content": "To use projects, open a workspace and add projects to it, or use the 'Add Project' feature and we will create a workspace for you.\n[Open Workspace](command:workbench.action.openWorkspace)\n[Add Project](command:projects.addProject)"
"projects-view-no-workspace-content": "No workspace open, create new or open existing to get started.\n[Create new](command:projects.new)\n[Open existing](command:projects.openExisting)\nTo learn more about SQL Database projects [read our docs](https://aka.ms/azuredatastudio-sqlprojects)",
"projects-view-no-project-content": "No projects found in current workspace.\n[Create new](command:projects.new)\n[Open existing](command:projects.openExisting)\nTo learn more about SQL Database projects [read our docs](https://aka.ms/azuredatastudio-sqlprojects).\n",
"open-existing-command": "Open existing",
"projects.defaultProjectSaveLocation": "Full path to folder where new projects are saved by default."
}

View File

@@ -7,9 +7,48 @@ import { EOL } from 'os';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export const ExtensionActivationErrorMessage = (extensionId: string, err: any): string => { return localize('activateExtensionFailed', "Failed to load the project provider extension '{0}'. Error message: {1}", extensionId, err.message ?? err); };
export const UnknownProjectsErrorMessage = (projectFiles: string[]): string => { return localize('UnknownProjectsError', "No provider was found for the following projects: {0}", projectFiles.join(EOL)); };
export const ExtensionActivationError = (extensionId: string, err: any): string => { return localize('activateExtensionFailed', "Failed to load the project provider extension '{0}'. Error message: {1}", extensionId, err.message ?? err); };
export const UnknownProjectsError = (projectFiles: string[]): string => { return localize('UnknownProjectsError', "No provider was found for the following projects: {0}", projectFiles.join(EOL)); };
export const SelectProjectFileActionName = localize('SelectProjectFileActionName', "Select");
export const AllProjectTypes = localize('AllProjectTypes', "All Project Types");
export const ProviderNotFoundForProjectTypeError = (projectType: string): string => { return localize('UnknownProjectTypeError', "No provider was found for project type with id: '{0}'", projectType); };
export const WorkspaceRequiredMessage = localize('dataworkspace.workspaceRequiredMessage', "A workspace is required in order to use the project feature.");
export const OpenWorkspace = localize('dataworkspace.openWorkspace', "Open Workspace…");
export const CreateWorkspaceConfirmation = localize('dataworkspace.createWorkspaceConfirmation', "A new workspace will be created and opened in order to open project. The Extension Host will restart and if there is a folder currently open, it will be closed.");
export const EnterWorkspaceConfirmation = localize('dataworkspace.enterWorkspaceConfirmation', "To open this workspace, the Extension Host will restart and if there is a workspace or folder currently open, it will be closed.");
// UI
export const OkButtonText = localize('dataworkspace.ok', "OK");
export const CancelButtonText = localize('dataworkspace.cancel', "Cancel");
export const BrowseButtonText = localize('dataworkspace.browse', "Browse");
export const DefaultInputWidth = '400px';
export const DefaultButtonWidth = '80px';
// New Project Dialog
export const NewProjectDialogTitle = localize('dataworkspace.NewProjectDialogTitle', "Create new project");
export const TypeTitle = localize('dataworkspace.Type', "Type");
export const ProjectNameTitle = localize('dataworkspace.projectNameTitle', "Name");
export const ProjectNamePlaceholder = localize('dataworkspace.projectNamePlaceholder', "Enter project name");
export const ProjectLocationTitle = localize('dataworkspace.projectLocationTitle', "Location");
export const ProjectLocationPlaceholder = localize('dataworkspace.projectLocationPlaceholder', "Select location to create project");
export const AddProjectToCurrentWorkspace = localize('dataworkspace.AddProjectToCurrentWorkspace', "This project will be added to the current workspace.");
export const NewWorkspaceWillBeCreated = localize('dataworkspace.NewWorkspaceWillBeCreated', "A new workspace will be created for this project.");
export const WorkspaceLocationTitle = localize('dataworkspace.workspaceLocationTitle', "Workspace location");
export const ProjectParentDirectoryNotExistError = (location: string): string => { return localize('dataworkspace.projectParentDirectoryNotExistError', "The selected location: '{0}' does not exist or is not a directory.", location); };
export const ProjectDirectoryAlreadyExistError = (projectName: string, location: string): string => { return localize('dataworkspace.projectDirectoryAlreadyExistError', "There is already a directory named '{0}' in the selected location: '{1}'.", projectName, location); };
//Open Existing Dialog
export const OpenExistingDialogTitle = localize('dataworkspace.openExistingDialogTitle', "Open existing");
export const ProjectFileNotExistError = (projectFilePath: string): string => { return localize('dataworkspace.projectFileNotExistError', "The selected project file '{0}' does not exist or is not a file.", projectFilePath); };
export const WorkspaceFileNotExistError = (workspaceFilePath: string): string => { return localize('dataworkspace.workspaceFileNotExistError', "The selected workspace file '{0}' does not exist or is not a file.", workspaceFilePath); };
export const Project = localize('dataworkspace.project', "Project");
export const Workspace = localize('dataworkspace.workspace', "Workspace");
export const LocationSelectorTitle = localize('dataworkspace.locationSelectorTitle', "Location");
export const ProjectFilePlaceholder = localize('dataworkspace.projectFilePlaceholder', "Select project (.sqlproj) file");
export const WorkspacePlaceholder = localize('dataworkspace.workspacePlaceholder', "Select workspace (.code-workspace) file");
export const WorkspaceFileExtension = 'code-workspace';
// Workspace settings for saving new projects
export const ProjectConfigurationKey = 'projects';
export const ProjectSaveLocationKey = 'defaultProjectSaveLocation';

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* 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 { IExtension } from 'dataworkspace';
import { WorkspaceService } from '../services/workspaceService';
import { defaultProjectSaveLocation } from './projectLocationHelper';
export class DataWorkspaceExtension implements IExtension {
constructor(private workspaceService: WorkspaceService) {
}
getProjectsInWorkspace(): vscode.Uri[] {
return this.workspaceService.getProjectsInWorkspace();
}
addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise<void> {
return this.workspaceService.addProjectsToWorkspace(projectFiles);
}
showProjectsView(): void {
vscode.commands.executeCommand('dataworkspace.views.main.focus');
}
get defaultProjectSaveLocation(): vscode.Uri | undefined {
return defaultProjectSaveLocation();
}
}

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* 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';
export interface IconPath {
dark: string;
light: string;
}
export class IconPathHelper {
private static extensionContext: vscode.ExtensionContext;
public static folder: IconPath;
public static setExtensionContext(extensionContext: vscode.ExtensionContext) {
IconPathHelper.extensionContext = extensionContext;
IconPathHelper.folder = IconPathHelper.makeIcon('folder', true);
}
private static makeIcon(name: string, sameIcon: boolean = false) {
const folder = 'images';
if (sameIcon) {
return {
dark: IconPathHelper.extensionContext.asAbsolutePath(`${folder}/${name}.svg`),
light: IconPathHelper.extensionContext.asAbsolutePath(`${folder}/${name}.svg`)
};
} else {
return {
dark: IconPathHelper.extensionContext.asAbsolutePath(`${folder}/dark/${name}.svg`),
light: IconPathHelper.extensionContext.asAbsolutePath(`${folder}/light/${name}.svg`)
};
}
}
}

View File

@@ -26,9 +26,15 @@ export interface IProjectProviderRegistry {
*/
readonly providers: IProjectProvider[];
/**
* Gets the project provider for the specified project extension
* @param extension The file extension of the project
*/
getProviderByProjectExtension(extension: string): IProjectProvider | undefined;
/**
* Gets the project provider for the specified project type
* @param projectType The project type, file extension of the project
* @param projectType The id of the project type
*/
getProviderByProjectType(projectType: string): IProjectProvider | undefined;
}
@@ -45,7 +51,7 @@ export interface IWorkspaceService {
/**
* Gets the project files in current workspace
*/
getProjectsInWorkspace(): Promise<vscode.Uri[]>;
getProjectsInWorkspace(): vscode.Uri[];
/**
* Gets the project provider by project file
@@ -65,8 +71,28 @@ export interface IWorkspaceService {
*/
removeProject(projectFile: vscode.Uri): Promise<void>;
/**
* Creates a new project from workspace
* @param name The name of the project
* @param location The location of the project
* @param projectTypeId The project type id
*/
createProject(name: string, location: vscode.Uri, projectTypeId: string): Promise<vscode.Uri>;
readonly isProjectProviderAvailable: boolean;
/**
* Event fires when projects in workspace changes
*/
readonly onDidWorkspaceProjectsChange: vscode.Event<void>;
/**
* Verify that a workspace is open or if one isn't, ask user to pick whether a workspace should be automatically created
*/
validateWorkspace(): Promise<boolean>;
/**
* Shows confirmation message that the extension host will be restarted and current workspace/file will be closed. If confirmed, the specified workspace will be entered.
*/
enterWorkspace(workspaceFile: vscode.Uri): Promise<void>;
}

View File

@@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* 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 fs from 'fs';
import * as constants from '../common/constants';
/**
* Returns the default location to save a new database project
*/
export function defaultProjectSaveLocation(): vscode.Uri | undefined {
return projectSaveLocationSettingIsValid() ? vscode.Uri.file(projectSaveLocationSetting()) : undefined;
}
/**
* Get workspace configurations for this extension
*/
function config(): vscode.WorkspaceConfiguration {
return vscode.workspace.getConfiguration(constants.ProjectConfigurationKey);
}
/**
* Returns the workspace setting on the default location to save new database projects
*/
function projectSaveLocationSetting(): string {
return config()[constants.ProjectSaveLocationKey];
}
/**
* Returns if the default save location for new database projects workspace setting exists and is
* a valid path
*/
function projectSaveLocationSettingIsValid(): boolean {
return projectSaveLocationSettingExists() && fs.existsSync(projectSaveLocationSetting());
}
/**
* Returns if a value for the default save location for new database projects exists
*/
function projectSaveLocationSettingExists(): boolean {
return projectSaveLocationSetting() !== undefined && projectSaveLocationSetting() !== null
&& projectSaveLocationSetting().trim() !== '';
}

View File

@@ -9,20 +9,24 @@ import { IProjectProviderRegistry } from './interfaces';
export const ProjectProviderRegistry: IProjectProviderRegistry = new class implements IProjectProviderRegistry {
private _providers = new Array<IProjectProvider>();
private _providerMapping: { [key: string]: IProjectProvider } = {};
private _providerFileExtensionMapping: { [key: string]: IProjectProvider } = {};
private _providerProjectTypeMapping: { [key: string]: IProjectProvider } = {};
registerProvider(provider: IProjectProvider): vscode.Disposable {
this.validateProvider(provider);
this._providers.push(provider);
provider.supportedProjectTypes.forEach(projectType => {
this._providerMapping[projectType.projectFileExtension.toUpperCase()] = provider;
this._providerFileExtensionMapping[projectType.projectFileExtension.toUpperCase()] = provider;
this._providerProjectTypeMapping[projectType.id.toUpperCase()] = provider;
});
return new vscode.Disposable(() => {
const idx = this._providers.indexOf(provider);
if (idx >= 0) {
this._providers.splice(idx, 1);
provider.supportedProjectTypes.forEach(projectType => {
delete this._providerMapping[projectType.projectFileExtension.toUpperCase()];
delete this._providerFileExtensionMapping[projectType.projectFileExtension.toUpperCase()];
delete this._providerProjectTypeMapping[projectType.id.toUpperCase()];
});
}
});
@@ -39,7 +43,11 @@ export const ProjectProviderRegistry: IProjectProviderRegistry = new class imple
validateProvider(provider: IProjectProvider): void {
}
getProviderByProjectExtension(extension: string): IProjectProvider | undefined {
return extension ? this._providerFileExtensionMapping[extension.toUpperCase()] : undefined;
}
getProviderByProjectType(projectType: string): IProjectProvider | undefined {
return projectType ? this._providerMapping[projectType.toUpperCase()] : undefined;
return projectType ? this._providerProjectTypeMapping[projectType.toUpperCase()] : undefined;
}
};

View File

@@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
export async function directoryExist(directoryPath: string): Promise<boolean> {
const stats = await getFileStatus(directoryPath);
return stats ? stats.isDirectory() : false;
}
export async function fileExist(filePath: string): Promise<boolean> {
const stats = await getFileStatus(filePath);
return stats ? stats.isFile() : false;
}
async function getFileStatus(path: string): Promise<fs.Stats | undefined> {
try {
const stats = await fs.promises.stat(path);
return stats;
}
catch (e) {
if (e.code === 'ENOENT') {
return undefined;
}
else {
throw e;
}
}
}

View File

@@ -5,7 +5,7 @@
import * as vscode from 'vscode';
import { IWorkspaceService } from './interfaces';
import { UnknownProjectsErrorMessage } from './constants';
import { UnknownProjectsError } from './constants';
import { WorkspaceTreeItem } from 'dataworkspace';
/**
@@ -37,6 +37,7 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider<Worksp
else {
// if the element is undefined return the project tree items
const projects = await this._workspaceService.getProjectsInWorkspace();
await vscode.commands.executeCommand('setContext', 'isProjectsViewEmpty', projects.length === 0);
const unknownProjects: string[] = [];
const treeItems: WorkspaceTreeItem[] = [];
for (const project of projects) {
@@ -60,7 +61,7 @@ export class WorkspaceTreeDataProvider implements vscode.TreeDataProvider<Worksp
});
}
if (unknownProjects.length > 0) {
vscode.window.showErrorMessage(UnknownProjectsErrorMessage(unknownProjects));
vscode.window.showErrorMessage(UnknownProjectsError(unknownProjects));
}
return treeItems;
}

View File

@@ -14,11 +14,25 @@ declare module 'dataworkspace' {
*/
export interface IExtension {
/**
* register a project provider
* @param provider new project provider
* @requires a disposable object, upon disposal, the provider will be unregistered.
* Returns all the projects in the workspace
*/
registerProjectProvider(provider: IProjectProvider): vscode.Disposable;
getProjectsInWorkspace(): vscode.Uri[];
/**
* Add projects to the workspace
* @param projectFiles Uris of project files to add
*/
addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise<void>
/**
* Change focus to Projects view
*/
showProjectsView(): void;
/**
* Returns the default location to save projects
*/
defaultProjectSaveLocation: vscode.Uri | undefined;
}
/**
@@ -37,6 +51,14 @@ declare module 'dataworkspace' {
*/
RemoveProject(projectFile: vscode.Uri): Promise<void>;
/**
*
* @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, projectTypeId: string): Promise<vscode.Uri>;
/**
* Gets the supported project types
*/
@@ -47,11 +69,21 @@ declare module 'dataworkspace' {
* Defines the project type
*/
export interface IProjectType {
/**
* id of the project type
*/
readonly id: string;
/**
* display name of the project type
*/
readonly displayName: string;
/**
* description of the project type
*/
readonly description: string;
/**
* project file extension, e.g. sqlproj
*/

View File

@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as path from 'path';
import * as constants from '../common/constants';
interface Deferred<T> {
resolve: (result: T | Promise<T>) => void;
reject: (reason: any) => void;
}
export abstract class DialogBase {
protected _toDispose: vscode.Disposable[] = [];
protected _dialogObject: azdata.window.Dialog;
protected initDialogComplete: Deferred<void> | undefined;
protected initDialogPromise: Promise<void> = new Promise<void>((resolve, reject) => this.initDialogComplete = { resolve, reject });
protected workspaceFormComponent: azdata.FormComponent | undefined;
protected workspaceInputBox: azdata.InputBoxComponent | undefined;
constructor(dialogTitle: string, dialogName: string, dialogWidth: azdata.window.DialogWidth = 600) {
this._dialogObject = azdata.window.createModelViewDialog(dialogTitle, dialogName, dialogWidth);
this._dialogObject.okButton.label = constants.OkButtonText;
this.register(this._dialogObject.cancelButton.onClick(() => this.onCancelButtonClicked()));
this.register(this._dialogObject.okButton.onClick(() => this.onOkButtonClicked()));
this._dialogObject.registerCloseValidator(async () => {
return this.validate();
});
}
protected abstract initialize(view: azdata.ModelView): Promise<void>;
protected async validate(): Promise<boolean> {
return Promise.resolve(true);
}
public async open(): Promise<void> {
const tab = azdata.window.createTab('');
tab.registerContent(async (view: azdata.ModelView) => {
return this.initialize(view);
});
this._dialogObject.content = [tab];
azdata.window.openDialog(this._dialogObject);
await this.initDialogPromise;
}
private onCancelButtonClicked(): void {
this.dispose();
}
private async onOkButtonClicked(): Promise<void> {
await this.onComplete();
this.dispose();
}
protected async onComplete(): Promise<void> {
}
protected dispose(): void {
this._toDispose.forEach(disposable => disposable.dispose());
}
protected register(disposable: vscode.Disposable): void {
this._toDispose.push(disposable);
}
protected showErrorMessage(message: string): void {
this._dialogObject.message = {
text: message,
level: azdata.window.MessageLevel.Error
};
}
protected createHorizontalContainer(view: azdata.ModelView, items: azdata.Component[]): azdata.FlexContainer {
return view.modelBuilder.flexContainer().withItems(items, { CSSStyles: { 'margin-right': '5px', 'margin-bottom': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
}
/**
* 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
*/
protected createWorkspaceContainer(view: azdata.ModelView): azdata.FormComponent {
const workspaceDescription = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: vscode.workspace.workspaceFile ? constants.AddProjectToCurrentWorkspace : constants.NewWorkspaceWillBeCreated,
CSSStyles: { 'margin-top': '3px', 'margin-bottom': '10px' }
}).component();
this.workspaceInputBox = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: constants.WorkspaceLocationTitle,
width: constants.DefaultInputWidth,
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 container = view.modelBuilder.flexContainer()
.withItems([workspaceDescription, this.workspaceInputBox])
.withLayout({ flexFlow: 'column' })
.component();
this.workspaceFormComponent = {
title: constants.Workspace,
component: container
};
return this.workspaceFormComponent;
}
/**
* Update the workspace inputbox based on the passed in location and name if there isn't a workspace currently open
* @param location
* @param name
*/
protected 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;
}
}
}

View File

@@ -0,0 +1,171 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as path from 'path';
import { DialogBase } from './dialogBase';
import { IWorkspaceService } from '../common/interfaces';
import * as constants from '../common/constants';
import { IProjectType } from 'dataworkspace';
import { directoryExist } from '../common/utils';
import { IconPathHelper } from '../common/iconHelper';
import { defaultProjectSaveLocation } from '../common/projectLocationHelper';
class NewProjectDialogModel {
projectTypeId: string = '';
projectFileExtension: string = '';
name: string = '';
location: string = '';
}
export class NewProjectDialog extends DialogBase {
public model: NewProjectDialogModel = new NewProjectDialogModel();
constructor(private workspaceService: IWorkspaceService) {
super(constants.NewProjectDialogTitle, 'NewProject');
}
async validate(): Promise<boolean> {
try {
// the selected location should be an existing directory
const parentDirectoryExists = await directoryExist(this.model.location);
if (!parentDirectoryExists) {
this.showErrorMessage(constants.ProjectParentDirectoryNotExistError(this.model.location));
return false;
}
// there shouldn't be an existing sub directory with the same name as the project in the selected location
const projectDirectoryExists = await directoryExist(path.join(this.model.location, this.model.name));
if (projectDirectoryExists) {
this.showErrorMessage(constants.ProjectDirectoryAlreadyExistError(this.model.name, this.model.location));
return false;
}
return true;
}
catch (err) {
this.showErrorMessage(err?.message ? err.message : err);
return false;
}
}
async onComplete(): Promise<void> {
try {
const validateWorkspace = await this.workspaceService.validateWorkspace();
if (validateWorkspace) {
await this.workspaceService.createProject(this.model.name, vscode.Uri.file(this.model.location), this.model.projectTypeId);
}
}
catch (err) {
vscode.window.showErrorMessage(err?.message ? err.message : err);
}
}
protected async initialize(view: azdata.ModelView): Promise<void> {
const allProjectTypes = await this.workspaceService.getAllProjectTypes();
const projectTypeRadioCardGroup = view.modelBuilder.radioCardGroup().withProperties<azdata.RadioCardGroupComponentProperties>({
cards: allProjectTypes.map((projectType: IProjectType) => {
return <azdata.RadioCard>{
id: projectType.id,
label: projectType.displayName,
icon: projectType.icon,
descriptions: [
{
textValue: projectType.displayName,
textStyles: {
'font-size': '13px',
'font-weight': 'bold'
}
}, {
textValue: projectType.description
}
]
};
}),
iconHeight: '75px',
iconWidth: '75px',
cardWidth: '170px',
cardHeight: '170px',
ariaLabel: constants.TypeTitle,
width: '500px',
iconPosition: 'top',
selectedCardId: allProjectTypes.length > 0 ? allProjectTypes[0].id : undefined
}).component();
this.register(projectTypeRadioCardGroup.onSelectionChanged((e) => {
this.model.projectTypeId = e.cardId;
}));
const projectNameTextBox = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: constants.ProjectNameTitle,
placeHolder: constants.ProjectNamePlaceholder,
required: true,
width: constants.DefaultInputWidth
}).component();
this.register(projectNameTextBox.onTextChanged(() => {
this.model.name = projectNameTextBox.value!;
projectNameTextBox.updateProperty('title', projectNameTextBox.value);
this.updateWorkspaceInputbox(this.model.location, this.model.name);
}));
const locationTextBox = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: constants.ProjectLocationTitle,
placeHolder: constants.ProjectLocationPlaceholder,
required: true,
width: constants.DefaultInputWidth
}).component();
this.register(locationTextBox.onTextChanged(() => {
this.model.location = locationTextBox.value!;
locationTextBox.updateProperty('title', locationTextBox.value);
this.updateWorkspaceInputbox(this.model.location, this.model.name);
}));
const browseFolderButton = view.modelBuilder.button().withProperties<azdata.ButtonProperties>({
ariaLabel: constants.BrowseButtonText,
iconPath: IconPathHelper.folder,
height: '16px',
width: '18px'
}).component();
this.register(browseFolderButton.onDidClick(async () => {
let folderUris = await vscode.window.showOpenDialog({
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
defaultUri: defaultProjectSaveLocation()
});
if (!folderUris || folderUris.length === 0) {
return;
}
const selectedFolder = folderUris[0].fsPath;
locationTextBox.value = selectedFolder;
this.model.location = selectedFolder;
this.updateWorkspaceInputbox(this.model.location, this.model.name);
}));
const form = view.modelBuilder.formContainer().withFormItems([
{
title: constants.TypeTitle,
required: true,
component: projectTypeRadioCardGroup
},
{
title: constants.ProjectNameTitle,
required: true,
component: this.createHorizontalContainer(view, [projectNameTextBox])
}, {
title: constants.ProjectLocationTitle,
required: true,
component: this.createHorizontalContainer(view, [locationTextBox, browseFolderButton])
},
this.createWorkspaceContainer(view)
]).component();
await view.initializeModel(form);
this.initDialogComplete?.resolve();
}
}

View File

@@ -0,0 +1,202 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as path from 'path';
import { DialogBase } from './dialogBase';
import * as constants from '../common/constants';
import { IWorkspaceService } from '../common/interfaces';
import { fileExist } from '../common/utils';
import { IconPathHelper } from '../common/iconHelper';
export class OpenExistingDialog extends DialogBase {
public _projectFile: string = '';
public _workspaceFile: string = '';
public _targetTypeRadioCardGroup: azdata.RadioCardGroupComponent | undefined;
public _filePathTextBox: azdata.InputBoxComponent | undefined;
public formBuilder: azdata.FormBuilder | undefined;
private _targetTypes = [
{
name: constants.Project,
icon: this.extensionContext.asAbsolutePath('images/Open_existing_Project.svg')
}, {
name: constants.Workspace,
icon: this.extensionContext.asAbsolutePath('images/Open_existing_Workspace.svg')
}
];
constructor(private workspaceService: IWorkspaceService, private extensionContext: vscode.ExtensionContext) {
super(constants.OpenExistingDialogTitle, 'OpenProject');
}
async validate(): Promise<boolean> {
try {
// the selected location should be an existing directory
if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Project) {
const fileExists = await fileExist(this._projectFile);
if (!fileExists) {
this.showErrorMessage(constants.ProjectFileNotExistError(this._projectFile));
return false;
}
} else if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
const fileExists = await fileExist(this._workspaceFile);
if (!fileExists) {
this.showErrorMessage(constants.WorkspaceFileNotExistError(this._workspaceFile));
return false;
}
}
return true;
}
catch (err) {
this.showErrorMessage(err?.message ? err.message : err);
return false;
}
}
async onComplete(): Promise<void> {
try {
if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
await this.workspaceService.enterWorkspace(vscode.Uri.file(this._workspaceFile));
} else {
const validateWorkspace = await this.workspaceService.validateWorkspace();
if (validateWorkspace) {
await this.workspaceService.addProjectsToWorkspace([vscode.Uri.file(this._projectFile)]);
}
}
}
catch (err) {
vscode.window.showErrorMessage(err?.message ? err.message : err);
}
}
protected async initialize(view: azdata.ModelView): Promise<void> {
this._targetTypeRadioCardGroup = view.modelBuilder.radioCardGroup().withProperties<azdata.RadioCardGroupComponentProperties>({
cards: this._targetTypes.map((targetType) => {
return <azdata.RadioCard>{
id: targetType.name,
label: targetType.name,
icon: targetType.icon,
descriptions: [
{
textValue: targetType.name,
textStyles: {
'font-size': '13px'
}
}
]
};
}),
iconHeight: '100px',
iconWidth: '100px',
cardWidth: '170px',
cardHeight: '170px',
ariaLabel: constants.TypeTitle,
width: '500px',
iconPosition: 'top',
selectedCardId: constants.Project
}).component();
this._filePathTextBox = view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: constants.LocationSelectorTitle,
placeHolder: constants.ProjectFilePlaceholder,
required: true,
width: constants.DefaultInputWidth
}).component();
this.register(this._filePathTextBox.onTextChanged(() => {
this._projectFile = this._filePathTextBox!.value!;
this._filePathTextBox!.updateProperty('title', this._projectFile);
this.updateWorkspaceInputbox(path.dirname(this._projectFile), path.basename(this._projectFile, path.extname(this._projectFile)));
}));
const browseFolderButton = view.modelBuilder.button().withProperties<azdata.ButtonProperties>({
ariaLabel: constants.BrowseButtonText,
iconPath: IconPathHelper.folder,
width: '18px',
height: '16px',
}).component();
this.register(browseFolderButton.onDidClick(async () => {
if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Project) {
await this.projectBrowse();
} else if (this._targetTypeRadioCardGroup?.selectedCardId === constants.Workspace) {
await this.workspaceBrowse();
}
}));
this.register(this._targetTypeRadioCardGroup.onSelectionChanged(({ cardId }) => {
if (cardId === constants.Project) {
this._filePathTextBox!.placeHolder = constants.ProjectFilePlaceholder;
this.formBuilder?.addFormItem(this.workspaceFormComponent!);
} else if (cardId === constants.Workspace) {
this._filePathTextBox!.placeHolder = constants.WorkspacePlaceholder;
this.formBuilder?.removeFormItem(this.workspaceFormComponent!);
}
// clear selected file textbox
this._filePathTextBox!.value = '';
}));
this.formBuilder = view.modelBuilder.formContainer().withFormItems([
{
title: constants.TypeTitle,
required: true,
component: this._targetTypeRadioCardGroup,
}, {
title: constants.LocationSelectorTitle,
required: true,
component: this.createHorizontalContainer(view, [this._filePathTextBox, browseFolderButton])
},
this.createWorkspaceContainer(view)
]);
await view.initializeModel(this.formBuilder?.component());
this.initDialogComplete?.resolve();
}
public async workspaceBrowse(): Promise<void> {
const filters: { [name: string]: string[] } = { [constants.Workspace]: [constants.WorkspaceFileExtension] };
const fileUris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
openLabel: constants.SelectProjectFileActionName,
filters: filters
});
if (!fileUris || fileUris.length === 0) {
return;
}
const workspaceFilePath = fileUris[0].fsPath;
this._filePathTextBox!.value = workspaceFilePath;
this._workspaceFile = workspaceFilePath;
}
public async projectBrowse(): Promise<void> {
const filters: { [name: string]: string[] } = {};
const projectTypes = await this.workspaceService.getAllProjectTypes();
filters[constants.AllProjectTypes] = projectTypes.map(type => type.projectFileExtension);
projectTypes.forEach(type => {
filters[type.displayName] = [type.projectFileExtension];
});
const fileUris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
openLabel: constants.SelectProjectFileActionName,
filters: filters
});
if (!fileUris || fileUris.length === 0) {
return;
}
const projectFilePath = fileUris[0].fsPath;
this._filePathTextBox!.value = projectFilePath;
this._projectFile = projectFilePath;
}
}

View File

@@ -4,48 +4,48 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as path from 'path';
import { WorkspaceTreeDataProvider } from './common/workspaceTreeDataProvider';
import { WorkspaceService } from './services/workspaceService';
import { AllProjectTypes, SelectProjectFileActionName } from './common/constants';
import { WorkspaceTreeItem } from 'dataworkspace';
import { WorkspaceTreeItem, IExtension } from 'dataworkspace';
import { DataWorkspaceExtension } from './common/dataWorkspaceExtension';
import { NewProjectDialog } from './dialogs/newProjectDialog';
import { OpenExistingDialog } from './dialogs/openExistingDialog';
import { IWorkspaceService } from './common/interfaces';
import { IconPathHelper } from './common/iconHelper';
export function activate(context: vscode.ExtensionContext): void {
const workspaceService = new WorkspaceService();
export function activate(context: vscode.ExtensionContext): Promise<IExtension> {
const workspaceService = new WorkspaceService(context);
workspaceService.loadTempProjects();
const workspaceTreeDataProvider = new WorkspaceTreeDataProvider(workspaceService);
const dataWorkspaceExtension = new DataWorkspaceExtension(workspaceService);
context.subscriptions.push(vscode.window.registerTreeDataProvider('dataworkspace.views.main', workspaceTreeDataProvider));
context.subscriptions.push(vscode.commands.registerCommand('projects.addProject', async () => {
// To Sakshi - You can replace the implementation with your complete dialog implementation
// but all the code here should be reusable by you
if (vscode.workspace.workspaceFile) {
const filters: { [name: string]: string[] } = {};
const projectTypes = await workspaceService.getAllProjectTypes();
filters[AllProjectTypes] = projectTypes.map(type => type.projectFileExtension);
projectTypes.forEach(type => {
filters[type.displayName] = [type.projectFileExtension];
});
let fileUris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: vscode.Uri.file(path.dirname(vscode.workspace.workspaceFile.path)),
openLabel: SelectProjectFileActionName,
filters: filters
});
if (!fileUris || fileUris.length === 0) {
return;
}
await workspaceService.addProjectsToWorkspace(fileUris);
}
context.subscriptions.push(vscode.extensions.onDidChange(() => {
setProjectProviderContextValue(workspaceService);
}));
setProjectProviderContextValue(workspaceService);
context.subscriptions.push(vscode.commands.registerCommand('projects.new', async () => {
const dialog = new NewProjectDialog(workspaceService);
await dialog.open();
}));
context.subscriptions.push(vscode.commands.registerCommand('projects.openExisting', async () => {
const dialog = new OpenExistingDialog(workspaceService, context);
await dialog.open();
}));
context.subscriptions.push(vscode.commands.registerCommand('dataworkspace.refresh', () => {
workspaceTreeDataProvider.refresh();
}));
context.subscriptions.push(vscode.commands.registerCommand('projects.removeProject', async (treeItem: WorkspaceTreeItem) => {
await workspaceService.removeProject(vscode.Uri.file(treeItem.element.project.projectFilePath));
}));
IconPathHelper.setExtensionContext(context);
return Promise.resolve(dataWorkspaceExtension);
}
function setProjectProviderContextValue(workspaceService: IWorkspaceService): void {
vscode.commands.executeCommand('setContext', 'isProjectProviderAvailable', workspaceService.isProjectProviderAvailable);
}
export function deactivate(): void {

View File

@@ -3,50 +3,137 @@
* 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 dataworkspace from 'dataworkspace';
import * as path from 'path';
import * as constants from '../common/constants';
import { IWorkspaceService } from '../common/interfaces';
import { ProjectProviderRegistry } from '../common/projectProviderRegistry';
import Logger from '../common/logger';
import { ExtensionActivationErrorMessage } from '../common/constants';
const WorkspaceConfigurationName = 'dataworkspace';
const ProjectsConfigurationName = 'projects';
const TempProject = 'tempProject';
export class WorkspaceService implements IWorkspaceService {
private _onDidWorkspaceProjectsChange: vscode.EventEmitter<void> = new vscode.EventEmitter<void>();
readonly onDidWorkspaceProjectsChange: vscode.Event<void> = this._onDidWorkspaceProjectsChange?.event;
async addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise<void> {
if (vscode.workspace.workspaceFile) {
const currentProjects: vscode.Uri[] = await this.getProjectsInWorkspace();
const newWorkspaceFolders: string[] = [];
let newProjectFileAdded = false;
for (const projectFile of projectFiles) {
if (currentProjects.findIndex((p: vscode.Uri) => p.fsPath === projectFile.fsPath) === -1) {
currentProjects.push(projectFile);
newProjectFileAdded = true;
constructor(private _context: vscode.ExtensionContext) {
}
// if the relativePath and the original path is the same, that means the project file is not under
// any workspace folders, we should add the parent folder of the project file to the workspace
const relativePath = vscode.workspace.asRelativePath(projectFile, false);
if (vscode.Uri.file(relativePath).fsPath === projectFile.fsPath) {
newWorkspaceFolders.push(path.dirname(projectFile.path));
}
/**
* Load any temp project that needed to be loaded before the extension host was restarted
* which would happen if a workspace was created in order open or create a project
*/
async loadTempProjects(): Promise<void> {
const tempProjects: string[] | undefined = this._context.globalState.get(TempProject) ?? undefined;
if (tempProjects && vscode.workspace.workspaceFile) {
// add project to workspace now that the workspace has been created and saved
for (let project of tempProjects) {
await this.addProjectsToWorkspace([vscode.Uri.file(<string>project)]);
}
await this._context.globalState.update(TempProject, undefined);
}
}
/**
* Creates a new workspace in the same folder as the project. Because the extension host gets restared when
* a new workspace is created and opened, the project needs to be saved as the temp project that will be loaded
* when the extension gets restarted
* @param projectFileFsPath project to add to the workspace
*/
async CreateNewWorkspaceForProject(projectFileFsPath: string): Promise<void> {
// save temp project
await this._context.globalState.update(TempProject, [projectFileFsPath]);
// create a new workspace - the workspace file will be created in the same folder as the project
const workspaceFile = vscode.Uri.file(path.join(path.dirname(projectFileFsPath), `${path.parse(projectFileFsPath).name}.code-workspace`));
const projectFolder = vscode.Uri.file(path.dirname(projectFileFsPath));
await azdata.workspace.createWorkspace(projectFolder, workspaceFile);
}
get isProjectProviderAvailable(): boolean {
for (const extension of vscode.extensions.all) {
const projectTypes = extension.packageJSON.contributes && extension.packageJSON.contributes.projects as string[];
if (projectTypes && projectTypes.length > 0) {
return true;
}
}
return false;
}
/**
* Verify that a workspace is open or that if one isn't, it's ok to create a workspace
*/
async validateWorkspace(): Promise<boolean> {
if (!vscode.workspace.workspaceFile) {
const result = await vscode.window.showWarningMessage(constants.CreateWorkspaceConfirmation, constants.OkButtonText, constants.CancelButtonText);
if (result === constants.OkButtonText) {
return true;
} else {
return false;
}
} else {
// workspace is open
return true;
}
}
/**
* Shows confirmation message that the extension host will be restarted and current workspace/file will be closed. If confirmed, the specified workspace will be entered.
* @param workspaceFile
*/
async enterWorkspace(workspaceFile: vscode.Uri): Promise<void> {
const result = await vscode.window.showWarningMessage(constants.EnterWorkspaceConfirmation, constants.OkButtonText, constants.CancelButtonText);
if (result === constants.OkButtonText) {
await azdata.workspace.enterWorkspace(workspaceFile);
} else {
return;
}
}
async addProjectsToWorkspace(projectFiles: vscode.Uri[]): Promise<void> {
if (!projectFiles || projectFiles.length === 0) {
return;
}
// a workspace needs to be open to add projects
if (!vscode.workspace.workspaceFile) {
await this.CreateNewWorkspaceForProject(projectFiles[0].fsPath);
// this won't get hit since the extension host will get restarted, but helps with testing
return;
}
const currentProjects: vscode.Uri[] = this.getProjectsInWorkspace();
const newWorkspaceFolders: string[] = [];
let newProjectFileAdded = false;
for (const projectFile of projectFiles) {
if (currentProjects.findIndex((p: vscode.Uri) => p.fsPath === projectFile.fsPath) === -1) {
currentProjects.push(projectFile);
newProjectFileAdded = true;
// if the relativePath and the original path is the same, that means the project file is not under
// any workspace folders, we should add the parent folder of the project file to the workspace
const relativePath = vscode.workspace.asRelativePath(projectFile, false);
if (vscode.Uri.file(relativePath).fsPath === projectFile.fsPath) {
newWorkspaceFolders.push(path.dirname(projectFile.path));
}
}
}
if (newProjectFileAdded) {
// Save the new set of projects to the workspace configuration.
await this.setWorkspaceConfigurationValue(ProjectsConfigurationName, currentProjects.map(project => this.toRelativePath(project)));
this._onDidWorkspaceProjectsChange.fire();
}
if (newProjectFileAdded) {
// Save the new set of projects to the workspace configuration.
await this.setWorkspaceConfigurationValue(ProjectsConfigurationName, currentProjects.map(project => this.toRelativePath(project)));
this._onDidWorkspaceProjectsChange.fire();
}
if (newWorkspaceFolders.length > 0) {
// second parameter is null means don't remove any workspace folders
vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders!.length, null, ...(newWorkspaceFolders.map(folder => ({ uri: vscode.Uri.file(folder) }))));
}
if (newWorkspaceFolders.length > 0) {
// second parameter is null means don't remove any workspace folders
vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders!.length, null, ...(newWorkspaceFolders.map(folder => ({ uri: vscode.Uri.file(folder) }))));
}
}
@@ -59,22 +146,22 @@ export class WorkspaceService implements IWorkspaceService {
return projectTypes;
}
async getProjectsInWorkspace(): Promise<vscode.Uri[]> {
getProjectsInWorkspace(): vscode.Uri[] {
return vscode.workspace.workspaceFile ? this.getWorkspaceConfigurationValue<string[]>(ProjectsConfigurationName).map(project => this.toUri(project)) : [];
}
async getProjectProvider(projectFile: vscode.Uri): Promise<dataworkspace.IProjectProvider | undefined> {
const projectType = path.extname(projectFile.path).replace(/\./g, '');
let provider = ProjectProviderRegistry.getProviderByProjectType(projectType);
let provider = ProjectProviderRegistry.getProviderByProjectExtension(projectType);
if (!provider) {
await this.ensureProviderExtensionLoaded(projectType);
}
return ProjectProviderRegistry.getProviderByProjectType(projectType);
return ProjectProviderRegistry.getProviderByProjectExtension(projectType);
}
async removeProject(projectFile: vscode.Uri): Promise<void> {
if (vscode.workspace.workspaceFile) {
const currentProjects: vscode.Uri[] = await this.getProjectsInWorkspace();
const currentProjects: vscode.Uri[] = this.getProjectsInWorkspace();
const projectIdx = currentProjects.findIndex((p: vscode.Uri) => p.fsPath === projectFile.fsPath);
if (projectIdx !== -1) {
currentProjects.splice(projectIdx, 1);
@@ -84,6 +171,18 @@ 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, projectTypeId);
this.addProjectsToWorkspace([projectFile]);
this._onDidWorkspaceProjectsChange.fire();
return projectFile;
} else {
throw new Error(constants.ProviderNotFoundForProjectTypeError(projectTypeId));
}
}
/**
* Ensure the project provider extension for the specified project is loaded
* @param projectType The file extension of the project, if not specified, all project provider extensions will be loaded.
@@ -113,7 +212,7 @@ export class WorkspaceService implements IWorkspaceService {
await extension.activate();
}
} catch (err) {
Logger.error(ExtensionActivationErrorMessage(extension.id, err));
Logger.error(constants.ExtensionActivationError(extension.id, err));
}
if (extension.isActive && extension.exports && !ProjectProviderRegistry.providers.includes(extension.exports)) {

View File

@@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import * as TypeMoq from 'typemoq';
import * as os from 'os';
import * as path from 'path';
import * as vscode from 'vscode';
import * as sinon from 'sinon';
import { promises as fs } from 'fs';
import { NewProjectDialog } from '../../dialogs/newProjectDialog';
import { WorkspaceService } from '../../services/workspaceService';
import { testProjectType } from '../testUtils';
suite('New Project Dialog', function (): void {
test('Should validate project location', async function (): Promise<void> {
const workspaceServiceMock = TypeMoq.Mock.ofType<WorkspaceService>();
workspaceServiceMock.setup(x => x.getAllProjectTypes()).returns(() => Promise.resolve([testProjectType]));
const dialog = new NewProjectDialog(workspaceServiceMock.object);
await dialog.open();
dialog.model.name = 'TestProject';
dialog.model.location = '';
should.equal(await dialog.validate(), false, 'Validation should fail becausee the parent directory does not exist');
// create a folder with the same name
const folderPath = path.join(os.tmpdir(), dialog.model.name);
await fs.mkdir(folderPath, { recursive: true });
dialog.model.location = os.tmpdir();
should.equal(await dialog.validate(), false, 'Validation should fail because a folder with the same name exists');
// change project name to be unique
dialog.model.name = `TestProject_${new Date().getTime()}`;
should.equal(await dialog.validate(), true, 'Validation should pass because name is unique and parent directory exists');
});
test('Should validate workspace in onComplete', async function (): Promise<void> {
const workspaceServiceMock = TypeMoq.Mock.ofType<WorkspaceService>();
workspaceServiceMock.setup(x => x.validateWorkspace()).returns(() => Promise.resolve(true));
workspaceServiceMock.setup(x => x.getAllProjectTypes()).returns(() => Promise.resolve([testProjectType]));
const dialog = new NewProjectDialog(workspaceServiceMock.object);
await dialog.open();
dialog.model.name = 'TestProject';
dialog.model.location = '';
should.doesNotThrow(async () => await dialog.onComplete());
workspaceServiceMock.setup(x => x.validateWorkspace()).throws(new Error('test error'));
const spy = sinon.spy(vscode.window, 'showErrorMessage');
should.doesNotThrow(async () => await dialog.onComplete(), 'Error should be caught');
should(spy.calledOnce).be.true();
});
});

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