Compare commits
91 Commits
1.24.0_rel
...
1.25.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d4917d328 | ||
|
|
edea311757 | ||
|
|
e7eacc32c0 | ||
|
|
12f50cca8d | ||
|
|
88a4dba695 | ||
|
|
634ea0ab6a | ||
|
|
cd0b5cbc7a | ||
|
|
0b7de6608a | ||
|
|
8c6bd8c857 | ||
|
|
def5775e00 | ||
|
|
91da9aea98 | ||
|
|
1c898402f8 | ||
|
|
54210cf479 | ||
|
|
cbcea87a82 | ||
|
|
2d50d2c5d1 | ||
|
|
7448c6c32c | ||
|
|
3196cf5be0 | ||
|
|
666726a5fa | ||
|
|
818a3d204e | ||
|
|
d45758b4f4 | ||
|
|
1eda5eb33a | ||
|
|
6ac5b7c8a5 | ||
|
|
397354ebc3 | ||
|
|
2a7b90fd70 | ||
|
|
1554e51932 | ||
|
|
d060f1b9a0 | ||
|
|
c8632c255a | ||
|
|
2ac03b9ef4 | ||
|
|
a0d89449cc | ||
|
|
b03a914934 | ||
|
|
d3bcb942f5 | ||
|
|
84822b23ac | ||
|
|
40ca82c63d | ||
|
|
f4a6b42b3a | ||
|
|
7ad631d4a5 | ||
|
|
4b7baa652f | ||
|
|
148e802f4a | ||
|
|
7bb4d00073 | ||
|
|
3b20e8a61c | ||
|
|
6e0a4f27de | ||
|
|
21ddf30a7b | ||
|
|
2ade45858e | ||
|
|
e0b1a3460d | ||
|
|
f44c714cf2 | ||
|
|
f72e12fe32 | ||
|
|
0b6fb504dc | ||
|
|
145b2491df | ||
|
|
aa30b52d03 | ||
|
|
6edcbbb738 | ||
|
|
815c61315c | ||
|
|
172a044ba7 | ||
|
|
2a81a0a70f | ||
|
|
749989cd0b | ||
|
|
8d42182db8 | ||
|
|
bb2a1db6e8 | ||
|
|
c579ecb111 | ||
|
|
175d46d508 | ||
|
|
870ff39527 | ||
|
|
ddd0b8b4bc | ||
|
|
c7cca5afea | ||
|
|
e63e4f0901 | ||
|
|
ddc8c00090 | ||
|
|
34170e7741 | ||
|
|
f5e4b32d01 | ||
|
|
28fef53731 | ||
|
|
438bc67072 | ||
|
|
472c9decfa | ||
|
|
6cd2d6c942 | ||
|
|
39e1181c5d | ||
|
|
c898b50b94 | ||
|
|
271fe62344 | ||
|
|
c18a54bc1d | ||
|
|
b57bf53b67 | ||
|
|
c699179e15 | ||
|
|
690937443c | ||
|
|
698b79f0f3 | ||
|
|
798af5fc2d | ||
|
|
af55dcfb42 | ||
|
|
76781d6cf4 | ||
|
|
99e3da5b48 | ||
|
|
6b657259a5 | ||
|
|
cbe2ba0901 | ||
|
|
b3d99117ca | ||
|
|
32ac586431 | ||
|
|
bd4676ac8c | ||
|
|
536628603e | ||
|
|
ea7fe08b98 | ||
|
|
6c920f6d54 | ||
|
|
a2f7136728 | ||
|
|
a082c1e478 | ||
|
|
32a6385fef |
@@ -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'];
|
||||
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -213,9 +213,9 @@ const externalExtensions = [
|
||||
'arc',
|
||||
'asde-deployment',
|
||||
'azdata',
|
||||
'azurehybridtoolkit',
|
||||
'cms',
|
||||
'dacpac',
|
||||
'data-workspace',
|
||||
'import',
|
||||
'kusto',
|
||||
'liveshare',
|
||||
|
||||
@@ -247,9 +247,9 @@ const externalExtensions = [
|
||||
'arc',
|
||||
'asde-deployment',
|
||||
'azdata',
|
||||
'azurehybridtoolkit',
|
||||
'cms',
|
||||
'dacpac',
|
||||
'data-workspace',
|
||||
'import',
|
||||
'kusto',
|
||||
'liveshare',
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
39
extensions/arc/src/common/kubeUtils.ts
Normal 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');
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
62
extensions/arc/src/test/common/kubeUtils.test.ts
Normal 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`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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.'); }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
91
extensions/arc/src/test/mocks/fakeRadioButton.ts
Normal 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
|
||||
|
||||
}
|
||||
@@ -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>();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
72
extensions/arc/src/ui/components/radioOptionsGroup.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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%"
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
extensions/azdata/src/typings/azdata-ext.d.ts
vendored
@@ -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>>
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
2
extensions/azurehybridtoolkit/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
notebooks/hybridbook/Components/**/obj
|
||||
*.vsix
|
||||
@@ -1,4 +1,7 @@
|
||||
.gitignore
|
||||
src/**
|
||||
out/**
|
||||
tsconfig.json
|
||||
extension.webpack.config.js
|
||||
*.vsix
|
||||
yarn.lock
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
BIN
extensions/azurehybridtoolkit/images/extension.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
@@ -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
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
---
|
||||
|
||||
@import 'main';
|
||||
@@ -1,4 +0,0 @@
|
||||
/* Put your custom CSS here */
|
||||
.left {
|
||||
margin-left: 0px;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
// Put your custom javascript here
|
||||
@@ -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…</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…</h1>
|
||||
<a href="{{ redirectURL }}">Click here if you are not redirected.</a>
|
||||
</html>
|
||||
@@ -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>
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
|
Before Width: | Height: | Size: 6.5 KiB |
@@ -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(">m")==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);
|
||||
@@ -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);
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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*.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"extends": "../shared.tsconfig.json",
|
||||
"compileOnSave": true,
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"outDir": "./out",
|
||||
"lib": [
|
||||
"es6", "es2015.promise"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
18
extensions/data-workspace/images/Open_existing_Project.svg
Normal 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 |
16
extensions/data-workspace/images/Open_existing_Workspace.svg
Normal 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 |
3
extensions/data-workspace/images/folder.svg
Normal 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 |
@@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
38
extensions/data-workspace/src/common/iconHelper.ts
Normal 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`)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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() !== '';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
31
extensions/data-workspace/src/common/utils.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
40
extensions/data-workspace/src/dataworkspace.d.ts
vendored
@@ -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
|
||||
*/
|
||||
|
||||
125
extensions/data-workspace/src/dialogs/dialogBase.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
171
extensions/data-workspace/src/dialogs/newProjectDialog.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
202
extensions/data-workspace/src/dialogs/openExistingDialog.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||