diff --git a/extensions/resource-deployment/images/sql_server_container.svg b/extensions/resource-deployment/images/sql_server_container.svg index 55fa302537..cc58ed2cf4 100644 --- a/extensions/resource-deployment/images/sql_server_container.svg +++ b/extensions/resource-deployment/images/sql_server_container.svg @@ -1,3 +1,9 @@ - - - \ No newline at end of file + + opac_command_icons_bv + + + + + + + diff --git a/extensions/resource-deployment/images/sql_server_container_inverse.svg b/extensions/resource-deployment/images/sql_server_container_inverse.svg index 849a6a0f2a..65131e60bb 100644 --- a/extensions/resource-deployment/images/sql_server_container_inverse.svg +++ b/extensions/resource-deployment/images/sql_server_container_inverse.svg @@ -1,31 +1,9 @@ - - - - - + + opac_command_icons_bv + + + + + + diff --git a/extensions/resource-deployment/notebooks/bdc/2019/azdata/deploy-bdc-aks.ipynb b/extensions/resource-deployment/notebooks/bdc/2019/azdata/deploy-bdc-aks.ipynb new file mode 100644 index 0000000000..ed069217b4 --- /dev/null +++ b/extensions/resource-deployment/notebooks/bdc/2019/azdata/deploy-bdc-aks.ipynb @@ -0,0 +1,273 @@ +{ + "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": [ + "![Microsoft](https://raw.githubusercontent.com/microsoft/azuredatastudio/master/src/sql/media/microsoft-small-logo.png)\n", + " \n", + "## Create Azure Kubernetes Service cluster and deploy SQL Server 2019 Big Data Cluster\n", + " \n", + "This notebook walks through the process of creating a new Azure Kubernetes Service cluster first, and then deploys a SQL Server 2019 Big Data Cluster on the newly created AKS cluster." + ], + "metadata": { + "azdata_cell_guid": "4f6bc3bc-3592-420a-b534-384011189005" + } + }, + { + "cell_type": "code", + "source": [ + "import json,sys,os\n", + "def run_command(command):\n", + " print(\"Executing: \" + command)\n", + " !{command}\n", + " if _exit_code != 0:\n", + " sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n\\t{command}\\n')\n", + " print(f'Successfully executed: {command}')" + ], + "metadata": { + "azdata_cell_guid": "326645cf-022a-47f2-8aff-37de71da8955", + "tags": [ + "hide_input" + ] + }, + "outputs": [], + "execution_count": 1 + }, + { + "cell_type": "markdown", + "source": [ + "### **Set variables**\n", + "Generated by Azure Data Studio using the values collected in the Deploy Big Data Cluster wizard" + ], + "metadata": { + "azdata_cell_guid": "8716915b-1439-431b-ab0a-0221ef94cb7f" + } + }, + { + "cell_type": "markdown", + "source": [ + "### **Set password**" + ], + "metadata": { + "azdata_cell_guid": "b083aa8d-990c-4170-ba1d-247ba5c6ae76" + } + }, + { + "cell_type": "code", + "source": [ + "mssql_password = os.environ[\"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\"]" + ], + "metadata": { + "azdata_cell_guid": "de256ddd-b835-4eb6-8cfc-c1a6239b0726" + }, + "outputs": [], + "execution_count": 0 + }, + { + "cell_type": "markdown", + "source": [ + "### **Login to Azure**\n", + "\n", + "This will open a web browser window to enable credentials to be entered. If this cells is hanging forever, it might be because your Web browser windows is waiting for you to enter your Azure credentials!\n", + "" + ], + "metadata": { + "azdata_cell_guid": "baddf2d9-93ee-4c42-aaf1-b42116bb1912" + } + }, + { + "cell_type": "code", + "source": [ + "run_command(f'az login')" + ], + "metadata": { + "azdata_cell_guid": "8f1404a6-216d-49fb-b6ad-81beeea50083", + "tags": [ + "hide_input" + ] + }, + "outputs": [], + "execution_count": 5 + }, + { + "cell_type": "markdown", + "source": [ + "\n", + "### **Set active Azure subscription**" + ], + "metadata": { + "azdata_cell_guid": "230dc0f1-bf6e-474a-bfaa-aae6f8aad12e" + } + }, + { + "cell_type": "code", + "source": [ + "if azure_subscription_id != \"\":\n", + " run_command(f'az account set --subscription {azure_subscription_id}')\n", + "else:\n", + " print('Using the default Azure subscription', {azure_subscription_id})\n", + "run_command(f'az account show')" + ], + "metadata": { + "azdata_cell_guid": "ab230931-2e99-483b-a229-3847684a8c1c", + "tags": [ + "hide_input" + ] + }, + "outputs": [], + "execution_count": 6 + }, + { + "cell_type": "markdown", + "source": [ + "### **Create Azure resource group**" + ], + "metadata": { + "azdata_cell_guid": "d51db914-f484-489f-990d-72edb3065068" + } + }, + { + "cell_type": "code", + "source": [ + "run_command(f'az group create --name {azure_resource_group} --location {azure_region}')" + ], + "metadata": { + "azdata_cell_guid": "7c53eb23-c327-41bf-8936-bd34a02ebdd5", + "tags": [ + "hide_input" + ] + }, + "outputs": [], + "execution_count": 7 + }, + { + "cell_type": "markdown", + "source": [ + "### **Create AKS cluster**" + ], + "metadata": { + "azdata_cell_guid": "818eb705-71e2-4013-8420-44886a5468b2" + } + }, + { + "cell_type": "code", + "source": [ + "run_command(f'az aks create --name {aks_cluster_name} --resource-group {azure_resource_group} --generate-ssh-keys --node-vm-size {azure_vm_size} --node-count {azure_vm_count}')" + ], + "metadata": { + "azdata_cell_guid": "3cea1da0-0c18-4030-a5aa-79bc98a5a14d", + "tags": [ + "hide_input" + ] + }, + "outputs": [], + "execution_count": 8 + }, + { + "cell_type": "markdown", + "source": [ + "### **Set the new AKS cluster as current context**" + ], + "metadata": { + "azdata_cell_guid": "5ade8453-5e71-478f-b6b6-83c55626243d" + } + }, + { + "cell_type": "code", + "source": [ + "run_command(f'az aks get-credentials --resource-group {azure_resource_group} --name {aks_cluster_name} --admin --overwrite-existing')" + ], + "metadata": { + "azdata_cell_guid": "9ccb9adf-1cf6-4dcb-8bd9-7ae9a85c2437", + "tags": [ + "hide_input" + ] + }, + "outputs": [], + "execution_count": 9 + }, + { + "cell_type": "markdown", + "source": [ + "### **Create deployment configuration files**" + ], + "metadata": { + "azdata_cell_guid": "57eb69fb-c68f-4ba8-818d-ffbaa0bc7aec" + } + }, + { + "cell_type": "code", + "source": [ + "mssql_target_profile = 'ads-bdc-custom-profile'\n", + "if not os.path.exists(mssql_target_profile):\n", + " os.mkdir(mssql_target_profile)\n", + "bdcJsonObj = json.loads(bdc_json)\n", + "controlJsonObj = json.loads(control_json)\n", + "bdcJsonFile = open(f'{mssql_target_profile}/bdc.json', 'w')\n", + "bdcJsonFile.write(json.dumps(bdcJsonObj, indent = 4))\n", + "bdcJsonFile.close()\n", + "controlJsonFile = open(f'{mssql_target_profile}/control.json', 'w')\n", + "controlJsonFile.write(json.dumps(controlJsonObj, indent = 4))\n", + "controlJsonFile.close()\n", + "print(f'Created deployment configuration folder: {mssql_target_profile}')" + ], + "metadata": { + "azdata_cell_guid": "3fd73c04-8a79-4d08-9049-1dad30265558", + "tags": [ + "hide_input" + ] + }, + "outputs": [], + "execution_count": 10 + }, + { + "cell_type": "markdown", + "source": [ + "### **Create SQL Server 2019 Big Data Cluster**" + ], + "metadata": { + "azdata_cell_guid": "6e82fad8-0fd0-4952-87ce-3fea1edd98cb" + } + }, + { + "cell_type": "code", + "source": [ + "print (f'Creating SQL Server 2019 Big Data Cluster: {mssql_cluster_name} using configuration {mssql_target_profile}')\n", + "os.environ[\"ACCEPT_EULA\"] = 'yes'\n", + "os.environ[\"AZDATA_USERNAME\"] = mssql_username\n", + "os.environ[\"AZDATA_PASSWORD\"] = mssql_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\\tkubectl get pods -n {mssql_cluster_name} ')\n", + "run_command(f'azdata bdc create -c {mssql_target_profile}')" + ], + "metadata": { + "azdata_cell_guid": "c43ea026-ca5e-4e2a-8602-fcc786354168", + "tags": [ + "hide_input" + ] + }, + "outputs": [], + "execution_count": 11 + } + ] +} \ No newline at end of file diff --git a/extensions/resource-deployment/notebooks/bdc/2019/azdata/deploy-bdc-existing-aks.ipynb b/extensions/resource-deployment/notebooks/bdc/2019/azdata/deploy-bdc-existing-aks.ipynb new file mode 100644 index 0000000000..8222b00aac --- /dev/null +++ b/extensions/resource-deployment/notebooks/bdc/2019/azdata/deploy-bdc-existing-aks.ipynb @@ -0,0 +1,170 @@ +{ + "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": [ + "![Microsoft](https://raw.githubusercontent.com/microsoft/azuredatastudio/master/src/sql/media/microsoft-small-logo.png)\n", + " \n", + "## Deploy SQL Server 2019 Big Data Cluster on an existing Azure Kubernetes Service (AKS) cluster\n", + " \n", + "This notebook walks through the process of deploying a SQL Server 2019 Big Data Cluster on an existing AKS cluster." + ], + "metadata": { + "azdata_cell_guid": "82e60c1a-7acf-47ee-877f-9e85e92e11da" + } + }, + { + "cell_type": "code", + "source": [ + "import json,sys,os\n", + "def run_command(command):\n", + " print(\"Executing: \" + command)\n", + " !{command}\n", + " if _exit_code != 0:\n", + " sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n\\t{command}\\n')\n", + " print(f'Successfully executed: {command}')" + ], + "metadata": { + "azdata_cell_guid": "d973d5b4-7f0a-4a9d-b204-a16480f3940d", + "tags": [] + }, + "outputs": [], + "execution_count": 1 + }, + { + "cell_type": "markdown", + "source": [ + "### **Set variables**\n", + "Generated by Azure Data Studio using the values collected in the Deploy Big Data Cluster wizard" + ], + "metadata": { + "azdata_cell_guid": "4b266b2d-bd1b-4565-92c9-3fc146cdce6d" + } + }, + { + "cell_type": "markdown", + "source": [ + "### **Set password**" + ], + "metadata": { + "azdata_cell_guid": "7c37d248-b9ac-4ad6-be56-158cd70443b1" + } + }, + { + "cell_type": "code", + "source": [ + "mssql_password = os.environ[\"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\"]" + ], + "metadata": { + "azdata_cell_guid": "83d455f3-db10-48bb-bb81-78a6b4e5f2fd" + }, + "outputs": [], + "execution_count": 0 + }, + { + "cell_type": "markdown", + "source": [ + "### **Set and show current context**" + ], + "metadata": { + "azdata_cell_guid": "127c8042-181f-4862-a390-96e59c181d09" + } + }, + { + "cell_type": "code", + "source": [ + "run_command(f'kubectl config use-context {mssql_cluster_context}')\n", + "run_command('kubectl config current-context')" + ], + "metadata": { + "azdata_cell_guid": "7d1a03d4-1df8-48eb-bff0-0042603b95b1", + "tags": [ + "hide_input" + ] + }, + "outputs": [], + "execution_count": 0 + }, + { + "cell_type": "markdown", + "source": [ + "### **Create deployment configuration files**" + ], + "metadata": { + "azdata_cell_guid": "138536c3-1db6-428f-9e5c-8269a02fb52e" + } + }, + { + "cell_type": "code", + "source": [ + "mssql_target_profile = 'ads-bdc-custom-profile'\n", + "if not os.path.exists(mssql_target_profile):\n", + " os.mkdir(mssql_target_profile)\n", + "bdcJsonObj = json.loads(bdc_json)\n", + "controlJsonObj = json.loads(control_json)\n", + "bdcJsonFile = open(f'{mssql_target_profile}/bdc.json', 'w')\n", + "bdcJsonFile.write(json.dumps(bdcJsonObj, indent = 4))\n", + "bdcJsonFile.close()\n", + "controlJsonFile = open(f'{mssql_target_profile}/control.json', 'w')\n", + "controlJsonFile.write(json.dumps(controlJsonObj, indent = 4))\n", + "controlJsonFile.close()\n", + "print(f'Created deployment configuration folder: {mssql_target_profile}')" + ], + "metadata": { + "azdata_cell_guid": "2ff82c8a-4bce-449c-9d91-3ac7dd272021", + "tags": [ + "hide_input" + ] + }, + "outputs": [], + "execution_count": 6 + }, + { + "cell_type": "markdown", + "source": [ + "### **Create SQL Server 2019 Big Data Cluster**" + ], + "metadata": { + "azdata_cell_guid": "efe78cd3-ed73-4c9b-b586-fdd6c07dd37f" + } + }, + { + "cell_type": "code", + "source": [ + "print (f'Creating SQL Server 2019 Big Data Cluster: {mssql_cluster_name} using configuration {mssql_target_profile}')\n", + "os.environ[\"ACCEPT_EULA\"] = 'yes'\n", + "os.environ[\"AZDATA_USERNAME\"] = mssql_username\n", + "os.environ[\"AZDATA_PASSWORD\"] = mssql_password\n", + "run_command(f'azdata bdc create -c {mssql_target_profile}')" + ], + "metadata": { + "azdata_cell_guid": "373947a1-90b9-49ee-86f4-17a4c7d4ca76", + "tags": [ + "hide_input" + ] + }, + "outputs": [], + "execution_count": 7 + } + ] +} \ No newline at end of file diff --git a/extensions/resource-deployment/notebooks/bdc/2019/azdata/deploy-bdc-existing-kubeadm.ipynb b/extensions/resource-deployment/notebooks/bdc/2019/azdata/deploy-bdc-existing-kubeadm.ipynb new file mode 100644 index 0000000000..94cff896ca --- /dev/null +++ b/extensions/resource-deployment/notebooks/bdc/2019/azdata/deploy-bdc-existing-kubeadm.ipynb @@ -0,0 +1,182 @@ +{ + "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": [ + "![Microsoft](https://raw.githubusercontent.com/microsoft/azuredatastudio/master/src/sql/media/microsoft-small-logo.png)\n", + " \n", + "## Deploy SQL Server 2019 Big Data Cluster on an existing cluster deployed using kubeadm\n", + " \n", + "This notebook walks through the process of deploying a SQL Server 2019 Big Data Cluster on an existing kubeadm cluster." + ], + "metadata": { + "azdata_cell_guid": "23954d96-3932-4a8e-ab73-da605f99b1a4" + } + }, + { + "cell_type": "code", + "source": [ + "import json,sys,os\n", + "def run_command(command):\n", + " print(\"Executing: \" + command)\n", + " !{command}\n", + " if _exit_code != 0:\n", + " sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n\\t{command}\\n')\n", + " print(f'Successfully executed: {command}')" + ], + "metadata": { + "azdata_cell_guid": "26fa8bc4-4b8e-4c31-ae11-50484821cea8", + "tags": [ + "hide_input" + ] + }, + "outputs": [], + "execution_count": 1 + }, + { + "cell_type": "markdown", + "source": [ + "### **Set variables**\n", + "Generated by Azure Data Studio using the values collected in the Deploy Big Data Cluster wizard" + ], + "metadata": { + "azdata_cell_guid": "e70640d0-6059-4cab-939e-e985a978c0da" + } + }, + { + "cell_type": "markdown", + "source": [ + "### **Set password**" + ], + "metadata": { + "azdata_cell_guid": "7b383b0d-5687-45b3-a16f-ba3b170c796e" + } + }, + { + "cell_type": "code", + "source": [ + "mssql_password = os.environ[\"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\"]\n", + "if mssql_auth_mode == \"ad\":\n", + " mssql_domain_service_account_password = os.environ[\"AZDATA_NB_VAR_BDC_AD_DOMAIN_SVC_PASSWORD\"]" + ], + "metadata": { + "azdata_cell_guid": "b5970f2b-cf13-41af-b0a2-5133d840325e", + "tags": [ + "hide_input" + ] + }, + "outputs": [], + "execution_count": 3 + }, + { + "cell_type": "markdown", + "source": [ + "### **Set and show current context**" + ], + "metadata": { + "azdata_cell_guid": "6456bd0c-5b64-4d76-be59-e3a5b32697f5" + } + }, + { + "cell_type": "code", + "source": [ + "run_command(f'kubectl config use-context {mssql_cluster_context}')\n", + "run_command('kubectl config current-context')" + ], + "metadata": { + "azdata_cell_guid": "a38f8b3a-f93a-484c-b9e2-4eba3ed99cc2", + "tags": [ + "hide_input" + ] + }, + "outputs": [], + "execution_count": 0 + }, + { + "cell_type": "markdown", + "source": [ + "### **Create deployment configuration files**" + ], + "metadata": { + "azdata_cell_guid": "6d78da36-6af5-4309-baad-bc81bb2cdb7f" + } + }, + { + "cell_type": "code", + "source": [ + "mssql_target_profile = 'ads-bdc-custom-profile'\n", + "if not os.path.exists(mssql_target_profile):\n", + " os.mkdir(mssql_target_profile)\n", + "bdcJsonObj = json.loads(bdc_json)\n", + "controlJsonObj = json.loads(control_json)\n", + "bdcJsonFile = open(f'{mssql_target_profile}/bdc.json', 'w')\n", + "bdcJsonFile.write(json.dumps(bdcJsonObj, indent = 4))\n", + "bdcJsonFile.close()\n", + "controlJsonFile = open(f'{mssql_target_profile}/control.json', 'w')\n", + "controlJsonFile.write(json.dumps(controlJsonObj, indent = 4))\n", + "controlJsonFile.close()\n", + "print(f'Created deployment configuration folder: {mssql_target_profile}')" + ], + "metadata": { + "azdata_cell_guid": "3110ab23-ecfc-4e36-a1c5-28536b7edebf", + "tags": [ + "hide_input" + ] + }, + "outputs": [], + "execution_count": 6 + }, + { + "cell_type": "markdown", + "source": [ + "### **Create SQL Server 2019 Big Data Cluster**" + ], + "metadata": { + "azdata_cell_guid": "7d56d262-8cd5-49e4-b745-332c6e7a3cb2" + } + }, + { + "cell_type": "code", + "source": [ + "print (f'Creating SQL Server 2019 Big Data Cluster: {mssql_cluster_name} using configuration {mssql_target_profile}')\n", + "os.environ[\"ACCEPT_EULA\"] = 'yes'\n", + "os.environ[\"AZDATA_USERNAME\"] = mssql_username\n", + "os.environ[\"AZDATA_PASSWORD\"] = mssql_password\n", + "if mssql_auth_mode == \"ad\":\n", + " os.environ[\"DOMAIN_SERVICE_ACCOUNT_USERNAME\"] = mssql_domain_service_account_username\n", + " os.environ[\"DOMAIN_SERVICE_ACCOUNT_PASSWORD\"] = mssql_domain_service_account_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\\tkubectl get pods -n {mssql_cluster_name} ')\n", + "run_command(f'azdata bdc create -c {mssql_target_profile}')" + ], + "metadata": { + "azdata_cell_guid": "0a743e88-e7d0-4b41-b8a3-e43985d15f2b", + "tags": [ + "hide_input" + ] + }, + "outputs": [], + "execution_count": 7 + } + ] +} \ No newline at end of file diff --git a/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-aks.ipynb b/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-aks.ipynb index 0e38dd2937..2b30472ff6 100644 --- a/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-aks.ipynb +++ b/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-aks.ipynb @@ -6,7 +6,7 @@ }, "language_info": { "name": "python", - "version": "3.7.3", + "version": "3.6.6", "mimetype": "text/x-python", "codemirror_mode": { "name": "ipython", @@ -85,11 +85,24 @@ "run_command('az --version')" ], "metadata": { - "azdata_cell_guid": "326645cf-022a-47f2-8aff-37de71da8955" + "azdata_cell_guid": "326645cf-022a-47f2-8aff-37de71da8955", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 1 }, + { + "cell_type": "markdown", + "source": [ + "### **Set variables**\n", + "Generated by Azure Data Studio using the values collected in the Deploy Big Data Cluster wizard" + ], + "metadata": { + "azdata_cell_guid": "8716915b-1439-431b-ab0a-0221ef94cb7f" + } + }, { "cell_type": "markdown", "source": [ @@ -115,21 +128,14 @@ "print('You can also use the controller password to access Knox and SQL Server.')" ], "metadata": { - "azdata_cell_guid": "17e5d087-7128-4d02-8c16-fe1ddee675e5" + "azdata_cell_guid": "17e5d087-7128-4d02-8c16-fe1ddee675e5", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 2 }, - { - "cell_type": "markdown", - "source": [ - "### **Set variables**\n", - "Generated by Azure Data Studio using the values collected in the Deploy Big Data Cluster wizard" - ], - "metadata": { - "azdata_cell_guid": "4945bace-a50a-4e58-b55c-e9736106f805" - } - }, { "cell_type": "markdown", "source": [ @@ -148,7 +154,10 @@ "run_command(f'az login')" ], "metadata": { - "azdata_cell_guid": "8f1404a6-216d-49fb-b6ad-81beeea50083" + "azdata_cell_guid": "8f1404a6-216d-49fb-b6ad-81beeea50083", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 5 @@ -173,7 +182,10 @@ "run_command(f'az account show')" ], "metadata": { - "azdata_cell_guid": "ab230931-2e99-483b-a229-3847684a8c1c" + "azdata_cell_guid": "ab230931-2e99-483b-a229-3847684a8c1c", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 6 @@ -193,7 +205,10 @@ "run_command(f'az group create --name {azure_resource_group} --location {azure_region}')" ], "metadata": { - "azdata_cell_guid": "7c53eb23-c327-41bf-8936-bd34a02ebdd5" + "azdata_cell_guid": "7c53eb23-c327-41bf-8936-bd34a02ebdd5", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 7 @@ -213,7 +228,10 @@ "run_command(f'az aks create --name {aks_cluster_name} --resource-group {azure_resource_group} --generate-ssh-keys --node-vm-size {azure_vm_size} --node-count {azure_vm_count}')" ], "metadata": { - "azdata_cell_guid": "3cea1da0-0c18-4030-a5aa-79bc98a5a14d" + "azdata_cell_guid": "3cea1da0-0c18-4030-a5aa-79bc98a5a14d", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 8 @@ -233,7 +251,10 @@ "run_command(f'az aks get-credentials --resource-group {azure_resource_group} --name {aks_cluster_name} --admin --overwrite-existing')" ], "metadata": { - "azdata_cell_guid": "9ccb9adf-1cf6-4dcb-8bd9-7ae9a85c2437" + "azdata_cell_guid": "9ccb9adf-1cf6-4dcb-8bd9-7ae9a85c2437", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 9 @@ -250,7 +271,6 @@ { "cell_type": "code", "source": [ - "os.environ[\"ACCEPT_EULA\"] = 'yes'\n", "mssql_target_profile = 'ads-bdc-custom-profile'\n", "if not os.path.exists(mssql_target_profile):\n", " os.mkdir(mssql_target_profile)\n", @@ -265,7 +285,10 @@ "print(f'Created deployment configuration folder: {mssql_target_profile}')" ], "metadata": { - "azdata_cell_guid": "3fd73c04-8a79-4d08-9049-1dad30265558" + "azdata_cell_guid": "3fd73c04-8a79-4d08-9049-1dad30265558", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 10 @@ -283,14 +306,18 @@ "cell_type": "code", "source": [ "print (f'Creating SQL Server 2019 Big Data Cluster: {mssql_cluster_name} using configuration {mssql_target_profile}')\n", - "os.environ[\"CONTROLLER_USERNAME\"] = mssql_controller_username\n", - "os.environ[\"CONTROLLER_PASSWORD\"] = mssql_password\n", - "os.environ[\"MSSQL_SA_PASSWORD\"] = mssql_password\n", - "os.environ[\"KNOX_PASSWORD\"] = mssql_password\n", + "os.environ[\"ACCEPT_EULA\"] = 'yes'\n", + "os.environ[\"AZDATA_USERNAME\"] = mssql_username\n", + "os.environ[\"AZDATA_PASSWORD\"] = mssql_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\\tkubectl get pods -n {mssql_cluster_name} ')\n", "run_command(f'azdata bdc create -c {mssql_target_profile}')" ], "metadata": { - "azdata_cell_guid": "c43ea026-ca5e-4e2a-8602-fcc786354168" + "azdata_cell_guid": "c43ea026-ca5e-4e2a-8602-fcc786354168", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 11 @@ -307,10 +334,13 @@ { "cell_type": "code", "source": [ - "run_command(f'azdata login --cluster-name {mssql_cluster_name}')" + "run_command(f'azdata login -n {mssql_cluster_name}')" ], "metadata": { - "azdata_cell_guid": "5120c387-1088-435b-856e-e59f147c45a2" + "azdata_cell_guid": "5120c387-1088-435b-856e-e59f147c45a2", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 12 @@ -337,7 +367,10 @@ "display(HTML(endpointsDataFrame.to_html(index=False, render_links=True)))" ], "metadata": { - "azdata_cell_guid": "9a5d0aef-a8da-4845-b470-d714435f0304" + "azdata_cell_guid": "9a5d0aef-a8da-4845-b470-d714435f0304", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 13 @@ -357,16 +390,19 @@ "source": [ "sqlEndpoints = [x for x in endpoints if x['name'] == 'sql-server-master']\n", "if sqlEndpoints and len(sqlEndpoints) == 1:\n", - " connectionParameter = '{\"serverName\":\"' + sqlEndpoints[0]['endpoint'] + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":\"sa\",\"password\":' + json.dumps(mssql_password) + '}'\n", + " connectionParameter = '{\"serverName\":\"' + sqlEndpoints[0]['endpoint'] + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":' + json.dumps(mssql_username) + ',\"password\":' + json.dumps(mssql_password) + '}'\n", " display(HTML('
Click here to connect to SQL Server Master instance
'))\n", "else:\n", " sys.exit('Could not find the SQL Server Master instance endpoint.')" ], "metadata": { - "azdata_cell_guid": "1c9d1f2c-62ba-4070-920a-d30b67bdcc7c" + "azdata_cell_guid": "1c9d1f2c-62ba-4070-920a-d30b67bdcc7c", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 14 } ] -} +} \ No newline at end of file diff --git a/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-existing-aks.ipynb b/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-existing-aks.ipynb index 8d7d81ab87..650d1b2f16 100644 --- a/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-existing-aks.ipynb +++ b/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-existing-aks.ipynb @@ -6,7 +6,7 @@ }, "language_info": { "name": "python", - "version": "3.7.3", + "version": "3.6.6", "mimetype": "text/x-python", "codemirror_mode": { "name": "ipython", @@ -83,11 +83,24 @@ "run_command('azdata --version')" ], "metadata": { - "azdata_cell_guid": "d973d5b4-7f0a-4a9d-b204-a16480f3940d" + "azdata_cell_guid": "d973d5b4-7f0a-4a9d-b204-a16480f3940d", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 1 }, + { + "cell_type": "markdown", + "source": [ + "### **Set variables**\n", + "Generated by Azure Data Studio using the values collected in the Deploy Big Data Cluster wizard" + ], + "metadata": { + "azdata_cell_guid": "4b266b2d-bd1b-4565-92c9-3fc146cdce6d" + } + }, { "cell_type": "markdown", "source": [ @@ -113,21 +126,14 @@ "print('You can also use the controller password to access Knox and SQL Server.')" ], "metadata": { - "azdata_cell_guid": "e7e10828-6cae-45af-8c2f-1484b6d4f9ac" + "azdata_cell_guid": "e7e10828-6cae-45af-8c2f-1484b6d4f9ac", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 3 }, - { - "cell_type": "markdown", - "source": [ - "### **Set variables**\n", - "Generated by Azure Data Studio using the values collected in the Deploy Big Data Cluster wizard" - ], - "metadata": { - "azdata_cell_guid": "c009edfe-b964-4b28-beeb-02a2c65f9918" - } - }, { "cell_type": "markdown", "source": [ @@ -144,7 +150,10 @@ "run_command('kubectl config current-context')" ], "metadata": { - "azdata_cell_guid": "7d1a03d4-1df8-48eb-bff0-0042603b95b1" + "azdata_cell_guid": "7d1a03d4-1df8-48eb-bff0-0042603b95b1", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 0 @@ -161,7 +170,6 @@ { "cell_type": "code", "source": [ - "os.environ[\"ACCEPT_EULA\"] = 'yes'\n", "mssql_target_profile = 'ads-bdc-custom-profile'\n", "if not os.path.exists(mssql_target_profile):\n", " os.mkdir(mssql_target_profile)\n", @@ -176,7 +184,10 @@ "print(f'Created deployment configuration folder: {mssql_target_profile}')" ], "metadata": { - "azdata_cell_guid": "2ff82c8a-4bce-449c-9d91-3ac7dd272021" + "azdata_cell_guid": "2ff82c8a-4bce-449c-9d91-3ac7dd272021", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 6 @@ -194,14 +205,18 @@ "cell_type": "code", "source": [ "print (f'Creating SQL Server 2019 Big Data Cluster: {mssql_cluster_name} using configuration {mssql_target_profile}')\n", - "os.environ[\"CONTROLLER_USERNAME\"] = mssql_controller_username\n", - "os.environ[\"CONTROLLER_PASSWORD\"] = mssql_password\n", - "os.environ[\"MSSQL_SA_PASSWORD\"] = mssql_password\n", - "os.environ[\"KNOX_PASSWORD\"] = mssql_password\n", + "os.environ[\"ACCEPT_EULA\"] = 'yes'\n", + "os.environ[\"AZDATA_USERNAME\"] = mssql_username\n", + "os.environ[\"AZDATA_PASSWORD\"] = mssql_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\\tkubectl get pods -n {mssql_cluster_name} ')\n", "run_command(f'azdata bdc create -c {mssql_target_profile}')" ], "metadata": { - "azdata_cell_guid": "373947a1-90b9-49ee-86f4-17a4c7d4ca76" + "azdata_cell_guid": "373947a1-90b9-49ee-86f4-17a4c7d4ca76", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 7 @@ -218,10 +233,13 @@ { "cell_type": "code", "source": [ - "run_command(f'azdata login --cluster-name {mssql_cluster_name}')" + "run_command(f'azdata login -n {mssql_cluster_name}')" ], "metadata": { - "azdata_cell_guid": "79adda27-371d-4dcb-b867-db025f8162a5" + "azdata_cell_guid": "79adda27-371d-4dcb-b867-db025f8162a5", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 8 @@ -248,7 +266,10 @@ "display(HTML(endpointsDataFrame.to_html(index=False, render_links=True)))" ], "metadata": { - "azdata_cell_guid": "a2202494-fd6c-4534-987d-15c403a5096f" + "azdata_cell_guid": "a2202494-fd6c-4534-987d-15c403a5096f", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 9 @@ -268,16 +289,19 @@ "source": [ "sqlEndpoints = [x for x in endpoints if x['name'] == 'sql-server-master']\n", "if sqlEndpoints and len(sqlEndpoints) == 1:\n", - " connectionParameter = '{\"serverName\":\"' + sqlEndpoints[0]['endpoint'] + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":\"sa\",\"password\":' + json.dumps(mssql_password) + '}'\n", + " connectionParameter = '{\"serverName\":\"' + sqlEndpoints[0]['endpoint'] + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":' + json.dumps(mssql_username) + ',\"password\":' + json.dumps(mssql_password) + '}'\n", " display(HTML('
Click here to connect to SQL Server Master instance
'))\n", "else:\n", - " sys.exit('Could not find the SQL Server Master instance endpoint')" + " sys.exit('Could not find the SQL Server Master instance endpoint.')" ], "metadata": { - "azdata_cell_guid": "48342355-9d2b-4fa6-b1aa-3bc77d434dfa" + "azdata_cell_guid": "48342355-9d2b-4fa6-b1aa-3bc77d434dfa", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 10 } ] -} +} \ No newline at end of file diff --git a/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-existing-kubeadm.ipynb b/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-existing-kubeadm.ipynb index e0baeb7ab1..d88c982998 100644 --- a/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-existing-kubeadm.ipynb +++ b/extensions/resource-deployment/notebooks/bdc/2019/deploy-bdc-existing-kubeadm.ipynb @@ -6,7 +6,7 @@ }, "language_info": { "name": "python", - "version": "3.7.3", + "version": "3.6.6", "mimetype": "text/x-python", "codemirror_mode": { "name": "ipython", @@ -83,11 +83,24 @@ "run_command('azdata --version')" ], "metadata": { - "azdata_cell_guid": "26fa8bc4-4b8e-4c31-ae11-50484821cea8" + "azdata_cell_guid": "26fa8bc4-4b8e-4c31-ae11-50484821cea8", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 1 }, + { + "cell_type": "markdown", + "source": [ + "### **Set variables**\n", + "Generated by Azure Data Studio using the values collected in the Deploy Big Data Cluster wizard" + ], + "metadata": { + "azdata_cell_guid": "e70640d0-6059-4cab-939e-e985a978c0da" + } + }, { "cell_type": "markdown", "source": [ @@ -103,6 +116,8 @@ "invoked_by_wizard = \"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\" in os.environ\n", "if invoked_by_wizard:\n", " mssql_password = os.environ[\"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\"]\n", + " if mssql_auth_mode == \"ad\":\n", + " mssql_domain_service_account_password = os.environ[\"AZDATA_NB_VAR_BDC_AD_DOMAIN_SVC_PASSWORD\"]\n", "else:\n", " mssql_password = getpass.getpass(prompt = 'SQL Server 2019 Big Data Cluster controller password')\n", " if mssql_password == \"\":\n", @@ -110,24 +125,21 @@ " confirm_password = getpass.getpass(prompt = 'Confirm password')\n", " if mssql_password != confirm_password:\n", " sys.exit(f'Passwords do not match.')\n", + " if mssql_auth_mode == \"ad\":\n", + " mssql_domain_service_account_password = getpass.getpass(prompt = 'Domain service account password')\n", + " if mssql_domain_service_account_password == \"\":\n", + " sys.exit(f'Domain service account password is required.')\n", "print('You can also use the controller password to access Knox and SQL Server.')" ], "metadata": { - "azdata_cell_guid": "b5970f2b-cf13-41af-b0a2-5133d840325e" + "azdata_cell_guid": "b5970f2b-cf13-41af-b0a2-5133d840325e", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 3 }, - { - "cell_type": "markdown", - "source": [ - "### **Set variables**\n", - "Generated by Azure Data Studio using the values collected in the Deploy Big Data Cluster wizard" - ], - "metadata": { - "azdata_cell_guid": "1d28aac5-955d-4b15-8b9c-8d6ec2b588fe" - } - }, { "cell_type": "markdown", "source": [ @@ -161,7 +173,6 @@ { "cell_type": "code", "source": [ - "os.environ[\"ACCEPT_EULA\"] = 'yes'\n", "mssql_target_profile = 'ads-bdc-custom-profile'\n", "if not os.path.exists(mssql_target_profile):\n", " os.mkdir(mssql_target_profile)\n", @@ -176,7 +187,10 @@ "print(f'Created deployment configuration folder: {mssql_target_profile}')" ], "metadata": { - "azdata_cell_guid": "3110ab23-ecfc-4e36-a1c5-28536b7edebf" + "azdata_cell_guid": "3110ab23-ecfc-4e36-a1c5-28536b7edebf", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 6 @@ -194,14 +208,21 @@ "cell_type": "code", "source": [ "print (f'Creating SQL Server 2019 Big Data Cluster: {mssql_cluster_name} using configuration {mssql_target_profile}')\n", - "os.environ[\"CONTROLLER_USERNAME\"] = mssql_controller_username\n", - "os.environ[\"CONTROLLER_PASSWORD\"] = mssql_password\n", - "os.environ[\"MSSQL_SA_PASSWORD\"] = mssql_password\n", - "os.environ[\"KNOX_PASSWORD\"] = mssql_password\n", + "os.environ[\"ACCEPT_EULA\"] = 'yes'\n", + "os.environ[\"AZDATA_USERNAME\"] = mssql_username\n", + "os.environ[\"AZDATA_PASSWORD\"] = mssql_password\n", + "if mssql_auth_mode == \"ad\":\n", + " os.environ[\"DOMAIN_SERVICE_ACCOUNT_USERNAME\"] = mssql_domain_service_account_username\n", + " os.environ[\"DOMAIN_SERVICE_ACCOUNT_PASSWORD\"] = mssql_domain_service_account_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\\tkubectl get pods -n {mssql_cluster_name} ')\n", "run_command(f'azdata bdc create -c {mssql_target_profile}')" ], "metadata": { - "azdata_cell_guid": "0a743e88-e7d0-4b41-b8a3-e43985d15f2b" + "azdata_cell_guid": "0a743e88-e7d0-4b41-b8a3-e43985d15f2b", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 7 @@ -218,10 +239,13 @@ { "cell_type": "code", "source": [ - "run_command(f'azdata login --cluster-name {mssql_cluster_name}')" + "run_command(f'azdata login -n {mssql_cluster_name}')" ], "metadata": { - "azdata_cell_guid": "3a49909b-e09e-4e62-a825-c39de2cffc94" + "azdata_cell_guid": "3a49909b-e09e-4e62-a825-c39de2cffc94", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 8 @@ -248,7 +272,10 @@ "display(HTML(endpointsDataFrame.to_html(index=False, render_links=True)))" ], "metadata": { - "azdata_cell_guid": "2a8c8d5d-862c-4672-9309-38aa03afc4e6" + "azdata_cell_guid": "2a8c8d5d-862c-4672-9309-38aa03afc4e6", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 9 @@ -268,16 +295,19 @@ "source": [ "sqlEndpoints = [x for x in endpoints if x['name'] == 'sql-server-master']\n", "if sqlEndpoints and len(sqlEndpoints) == 1:\n", - " connectionParameter = '{\"serverName\":\"' + sqlEndpoints[0]['endpoint'] + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":\"sa\",\"password\":' + json.dumps(mssql_password) + '}'\n", + " connectionParameter = '{\"serverName\":\"' + sqlEndpoints[0]['endpoint'] + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":' + json.dumps(mssql_username) + ',\"password\":' + json.dumps(mssql_password) + '}'\n", " display(HTML('
Click here to connect to SQL Server Master instance
'))\n", "else:\n", - " sys.exit('Could not find the SQL Server Master instance endpoint')" + " sys.exit('Could not find the SQL Server Master instance endpoint.')" ], "metadata": { - "azdata_cell_guid": "d591785d-71aa-4c5d-9cbb-a7da79bca503" + "azdata_cell_guid": "d591785d-71aa-4c5d-9cbb-a7da79bca503", + "tags": [ + "hide_input" + ] }, "outputs": [], "execution_count": 10 } ] -} +} \ No newline at end of file diff --git a/extensions/resource-deployment/package.json b/extensions/resource-deployment/package.json index 08c6a1d843..8f33b346ed 100644 --- a/extensions/resource-deployment/package.json +++ b/extensions/resource-deployment/package.json @@ -26,16 +26,6 @@ ], "contributes": { "commands": [ - { - "command": "azdata.resource.sql-image.deploy", - "title": "%deploy-sql-image-command-name%", - "category": "%deploy-resource-command-category%" - }, - { - "command": "azdata.resource.sql-bdc.deploy", - "title": "%deploy-sql-bdc-command-name%", - "category": "%deploy-resource-command-category%" - }, { "command": "azdata.resource.deploy", "title": "%deploy-resource-command-name%", @@ -55,14 +45,6 @@ } ], "dataExplorer/action": [ - { - "command": "azdata.resource.sql-image.deploy", - "group": "secondary" - }, - { - "command": "azdata.resource.sql-bdc.deploy", - "group": "secondary" - }, { "command": "azdata.resource.deploy", "group": "secondary" @@ -72,6 +54,7 @@ "resourceDeploymentTypes": [ { "name": "sql-image", + "displayIndex": 2, "displayName": "%resource-type-sql-image-display-name%", "description": "%resource-type-sql-image-description%", "platforms": "*", @@ -202,6 +185,7 @@ }, { "name": "sql-bdc", + "displayIndex": 3, "displayName": "%resource-type-sql-bdc-display-name%", "description": "%resource-type-sql-bdc-description%", "platforms": "*", @@ -243,7 +227,8 @@ { "wizard": { "type": "new-aks", - "notebook": "%bdc-2019-aks-notebook%" + "notebook": "%bdc-2019-aks-notebook%", + "azdata_notebook": "%azdata-bdc-2019-aks-notebook%" }, "requiredTools": [ { @@ -261,7 +246,8 @@ { "wizard": { "type": "existing-aks", - "notebook": "%bdc-2019-existing-aks-notebook%" + "notebook": "%bdc-2019-existing-aks-notebook%", + "azdata_notebook": "%azdata-bdc-2019-existing-aks-notebook" }, "requiredTools": [ { @@ -276,7 +262,8 @@ { "wizard": { "type": "existing-kubeadm", - "notebook": "%bdc-2019-existing-kubeadm-notebook%" + "notebook": "%bdc-2019-existing-kubeadm-notebook%", + "azdata_notebook": "%azdata-bdc-2019-existing-kubeadm-notebook%" }, "requiredTools": [ { @@ -309,6 +296,7 @@ }, { "name": "sql-windows-setup", + "displayIndex": 1, "displayName": "%resource-type-sql-windows-setup-display-name%", "description": "%resource-type-sql-windows-setup-description%", "platforms": [ diff --git a/extensions/resource-deployment/package.nls.json b/extensions/resource-deployment/package.nls.json index b87e33b66a..4bad59b46b 100644 --- a/extensions/resource-deployment/package.nls.json +++ b/extensions/resource-deployment/package.nls.json @@ -1,12 +1,10 @@ { "extension-displayName": "SQL Server Deployment extension for Azure Data Studio", "extension-description": "Provides a notebook-based experience to deploy Microsoft SQL Server", - "deploy-sql-image-command-name": "Deploy SQL Server on Docker…", - "deploy-sql-bdc-command-name": "Deploy SQL Server Big Data Cluster…", - "deploy-resource-command-name": "More deployment options…", + "deploy-resource-command-name": "Deploy SQL Server…", "deploy-resource-command-category": "Deployment", "resource-type-sql-image-display-name": "SQL Server container image", - "resource-type-sql-image-description": "Run SQL Server container image with Docker", + "resource-type-sql-image-description": "Run SQL Server container image with docker", "resource-type-sql-bdc-display-name": "SQL Server Big Data Cluster", "resource-type-sql-bdc-description": "SQL Server Big Data Cluster allows you to deploy scalable clusters of SQL Server, Spark, and HDFS containers running on Kubernetes", "version-display-name": "Version", @@ -14,7 +12,7 @@ "sql-2019-display-name": "SQL Server 2019 RC", "sql-2017-docker-notebook": "./notebooks/docker/2017/deploy-sql2017-image.ipynb", "sql-2019-docker-notebook": "./notebooks/docker/2019/deploy-sql2019-image.ipynb", - "bdc-2019-display-name": "SQL Server 2019 RC Big Data Cluster", + "bdc-2019-display-name": "SQL Server 2019 RC", "bdc-deployment-target": "Deployment target", "bdc-deployment-target-new-aks": "New Azure Kubernetes Service Cluster", "bdc-deployment-target-existing-aks": "Existing Azure Kubernetes Service Cluster", @@ -22,8 +20,11 @@ "bdc-2019-aks-notebook": "./notebooks/bdc/2019/deploy-bdc-aks.ipynb", "bdc-2019-existing-aks-notebook": "./notebooks/bdc/2019/deploy-bdc-existing-aks.ipynb", "bdc-2019-existing-kubeadm-notebook": "./notebooks/bdc/2019/deploy-bdc-existing-kubeadm.ipynb", - "docker-sql-2017-title": "Deploy SQL Server 2017 container images with Docker", - "docker-sql-2019-title": "Deploy SQL Server 2019 container images with Docker", + "azdata-bdc-2019-aks-notebook": "./notebooks/bdc/2019/azdata/deploy-bdc-aks.ipynb", + "azdata-bdc-2019-existing-aks-notebook": "./notebooks/bdc/2019/azdata/deploy-bdc-existing-aks.ipynb", + "azdata-bdc-2019-existing-kubeadm-notebook": "./notebooks/bdc/2019/azdata/deploy-bdc-existing-kubeadm.ipynb", + "docker-sql-2017-title": "Deploy SQL Server 2017 container images with docker", + "docker-sql-2019-title": "Deploy SQL Server 2019 container images with docker", "docker-container-name-field": "Container name", "docker-sql-password-field": "SQL Server password", "docker-confirm-sql-password-field": "Confirm password", @@ -52,5 +53,5 @@ "bdc-agreement": "I accept {0}, {1} and {2}.", "bdc-agreement-privacy-statement":"Microsoft Privacy Statement", "bdc-agreement-azdata-eula":"azdata License Terms", - "bdc-agreement-bdc-eula":"SQL Server Big Data Cluster License Terms" + "bdc-agreement-bdc-eula":"SQL Server License Terms" } diff --git a/extensions/resource-deployment/src/interfaces.ts b/extensions/resource-deployment/src/interfaces.ts index 2d2fda804d..ebf4926e18 100644 --- a/extensions/resource-deployment/src/interfaces.ts +++ b/extensions/resource-deployment/src/interfaces.ts @@ -16,6 +16,7 @@ export interface ResourceType { options: ResourceTypeOption[]; providers: DeploymentProvider[]; agreement?: AgreementInfo; + displayIndex?: number; getProvider(selectedOptions: { option: string, value: string }[]): DeploymentProvider | undefined; } @@ -92,6 +93,7 @@ export type DeploymentProvider = DialogDeploymentProvider | WizardDeploymentProv export interface WizardInfo { notebook: string | NotebookInfo; + azdata_notebook: string | NotebookInfo; type: BdcDeploymentType; } @@ -161,6 +163,9 @@ export interface FieldInfo { useCustomValidator?: boolean; labelPosition?: LabelPosition; // overwrite the labelPosition of SectionInfo. fontStyle?: FontStyle; + labelFontWeight?: FontWeight; + links?: azdata.LinkArea[]; + editable?: boolean; // for editable dropdown } export enum LabelPosition { @@ -173,6 +178,11 @@ export enum FontStyle { Italic = 'italic' } +export enum FontWeight { + Normal = 'normal', + Bold = 'bold' +} + export enum FieldType { Text = 'text', Number = 'number', diff --git a/extensions/resource-deployment/src/services/azdataService.ts b/extensions/resource-deployment/src/services/azdataService.ts index 05f99e92a9..99bd7f83de 100644 --- a/extensions/resource-deployment/src/services/azdataService.ts +++ b/extensions/resource-deployment/src/services/azdataService.ts @@ -5,23 +5,42 @@ import * as path from 'path'; import { IPlatformService } from './platformService'; import { BigDataClusterDeploymentProfile } from './bigDataClusterDeploymentProfile'; +import { BdcDeploymentType } from '../interfaces'; interface BdcConfigListOutput { - stdout: string[]; + result: string[]; +} + +export interface BdcEndpoint { + endpoint: string; + name: 'sql-server-master'; } export interface IAzdataService { - getDeploymentProfiles(): Promise; + getDeploymentProfiles(deploymentType: BdcDeploymentType): Promise; + getEndpoints(clusterName: string, userName: string, password: string): Promise; } export class AzdataService implements IAzdataService { constructor(private platformService: IPlatformService) { } - public async getDeploymentProfiles(): Promise { + public async getDeploymentProfiles(deploymentType: BdcDeploymentType): Promise { + let profilePrefix: string; + switch (deploymentType) { + case BdcDeploymentType.NewAKS: + case BdcDeploymentType.ExistingAKS: + profilePrefix = 'aks'; + break; + case BdcDeploymentType.ExistingKubeAdm: + profilePrefix = 'kubeadm'; + break; + default: + throw new Error(`Unknown deployment type: ${deploymentType}`); + } await this.ensureWorkingDirectoryExists(); const profileNames = await this.getDeploymentProfileNames(); - return await Promise.all(profileNames.map(profile => this.getDeploymentProfileInfo(profile))); + return await Promise.all(profileNames.filter(profile => profile.startsWith(profilePrefix)).map(profile => this.getDeploymentProfileInfo(profile))); } private async getDeploymentProfileNames(): Promise { @@ -29,17 +48,16 @@ export class AzdataService implements IAzdataService { // azdata requires this environment variables to be set env['ACCEPT_EULA'] = 'yes'; const cmd = 'azdata bdc config list -o json'; - // Run the command twice to workaround the issue: - // First time use of the azdata will have extra EULA related string in the output - // there is no easy and reliable way to filter out the profile names from it. - await this.platformService.runCommand(cmd, { additionalEnvironmentVariables: env }); - const stdout = await this.platformService.runCommand(cmd); + const stdout = await this.platformService.runCommand(cmd, { additionalEnvironmentVariables: env }); const output = JSON.parse(stdout); - return output.stdout; + return output.result; } private async getDeploymentProfileInfo(profileName: string): Promise { - await this.platformService.runCommand(`azdata bdc config init --source ${profileName} --target ${profileName} --force`, { workingDirectory: this.platformService.storagePath() }); + const env: NodeJS.ProcessEnv = {}; + // azdata requires this environment variables to be set + env['ACCEPT_EULA'] = 'yes'; + await this.platformService.runCommand(`azdata bdc config init --source ${profileName} --target ${profileName} --force`, { workingDirectory: this.platformService.storagePath(), additionalEnvironmentVariables: env }); const configObjects = await Promise.all([ this.getJsonObjectFromFile(path.join(this.platformService.storagePath(), profileName, 'bdc.json')), this.getJsonObjectFromFile(path.join(this.platformService.storagePath(), profileName, 'control.json')) @@ -56,4 +74,16 @@ export class AzdataService implements IAzdataService { private async getJsonObjectFromFile(path: string): Promise { return JSON.parse(await this.platformService.readTextFile(path)); } + + public async getEndpoints(clusterName: string, userName: string, password: string): Promise { + const env: NodeJS.ProcessEnv = {}; + env['AZDATA_USERNAME'] = userName; + env['AZDATA_PASSWORD'] = password; + env['ACCEPT_EULA'] = 'yes'; + let cmd = 'azdata login -n ' + clusterName; + await this.platformService.runCommand(cmd, { additionalEnvironmentVariables: env }); + cmd = 'azdata bdc endpoint list'; + const stdout = await this.platformService.runCommand(cmd, { additionalEnvironmentVariables: env }); + return JSON.parse(stdout); + } } diff --git a/extensions/resource-deployment/src/services/bigDataClusterDeploymentProfile.ts b/extensions/resource-deployment/src/services/bigDataClusterDeploymentProfile.ts index fe311313fe..01abb5bda3 100644 --- a/extensions/resource-deployment/src/services/bigDataClusterDeploymentProfile.ts +++ b/extensions/resource-deployment/src/services/bigDataClusterDeploymentProfile.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - +import { AuthenticationMode } from '../ui/deployClusterWizard/deployClusterWizardModel'; export const SqlServerMasterResource = 'master'; export const DataResource = 'data-0'; export const HdfsResource = 'storage-0'; @@ -11,15 +11,26 @@ export const NameNodeResource = 'nmnode-0'; export const SparkHeadResource = 'sparkhead'; export const ZooKeeperResource = 'zookeeper'; export const SparkResource = 'spark-0'; -export const HadrEnabledSetting = 'hadr.enabled'; interface ServiceEndpoint { port: number; serviceType: ServiceType; name: EndpointName; + dnsName?: string; } type ServiceType = 'NodePort' | 'LoadBalancer'; -type EndpointName = 'Controller' | 'Master' | 'Knox' | 'MasterSecondary'; +type EndpointName = 'Controller' | 'Master' | 'Knox' | 'MasterSecondary' | 'AppServiceProxy' | 'ServiceProxy'; + +export interface ActiveDirectorySettings { + organizationalUnit: string; + domainControllerFQDNs: string; + dnsIPAddresses: string; + domainDNSName: string; + clusterUsers: string; + clusterAdmins: string; + appReaders?: string; + appOwners?: string; +} export class BigDataClusterDeploymentProfile { constructor(private _profileName: string, private _bdcConfig: any, private _controlConfig: any) { @@ -39,6 +50,30 @@ export class BigDataClusterDeploymentProfile { this._bdcConfig.metadata.name = value; } + public get registry(): string { + return this._controlConfig.spec.docker.registry; + } + + public set registry(value: string) { + this._controlConfig.spec.docker.registry = value; + } + + public get repository(): string { + return this._controlConfig.spec.docker.repository; + } + + public set repository(value: string) { + this._controlConfig.spec.docker.repository = value; + } + + public get imageTag(): string { + return this._controlConfig.spec.docker.imageTag; + } + + public set imageTag(value: string) { + this._controlConfig.spec.docker.imageTag = value; + } + public get bdcConfig(): any { return this._bdcConfig; } @@ -107,15 +142,6 @@ export class BigDataClusterDeploymentProfile { return this._bdcConfig.spec.resources[SparkResource] ? this.getReplicas(SparkResource) : 0; } - public get hadrEnabled(): boolean { - const value = this._bdcConfig.spec.resources[SqlServerMasterResource].spec.settings.sql[HadrEnabledSetting]; - return value === true || value === 'true'; - } - - public set hadrEnabled(value: boolean) { - this._bdcConfig.spec.resources[SqlServerMasterResource].spec.settings.sql[HadrEnabledSetting] = value; - } - public get includeSpark(): boolean { return this._bdcConfig.spec.resources[HdfsResource].spec.settings.spark.includeSpark; } @@ -175,32 +201,48 @@ export class BigDataClusterDeploymentProfile { return this.getEndpointPort(this._controlConfig.spec.endpoints, 'Controller', 30080); } - public set controllerPort(port: number) { - this.setEndpointPort(this._controlConfig.spec.endpoints, 'Controller', port); + public setControllerEndpoint(port: number, dnsName?: string) { + this.setEndpoint(this._controlConfig.spec.endpoints, 'Controller', port, dnsName); + } + + public get serviceProxyPort(): number { + return this.getEndpointPort(this._controlConfig.spec.endpoints, 'ServiceProxy', 30080); + } + + public setServiceProxyEndpoint(port: number, dnsName?: string) { + this.setEndpoint(this._controlConfig.spec.endpoints, 'ServiceProxy', port, dnsName); + } + + public get appServiceProxyPort(): number { + return this.getEndpointPort(this._bdcConfig.spec.resources.appproxy.spec.endpoints, 'AppServiceProxy', 30777); + } + + public setAppServiceProxyEndpoint(port: number, dnsName?: string) { + this.setEndpoint(this._bdcConfig.spec.resources.appproxy.spec.endpoints, 'AppServiceProxy', port, dnsName); } public get sqlServerPort(): number { return this.getEndpointPort(this._bdcConfig.spec.resources.master.spec.endpoints, 'Master', 31433); } - public set sqlServerPort(port: number) { - this.setEndpointPort(this._bdcConfig.spec.resources.master.spec.endpoints, 'Master', port); + public setSqlServerEndpoint(port: number, dnsName?: string) { + this.setEndpoint(this._bdcConfig.spec.resources.master.spec.endpoints, 'Master', port, dnsName); } public get sqlServerReadableSecondaryPort(): number { return this.getEndpointPort(this._bdcConfig.spec.resources.master.spec.endpoints, 'MasterSecondary', 31436); } - public set sqlServerReadableSecondaryPort(port: number) { - this.setEndpointPort(this._bdcConfig.spec.resources.master.spec.endpoints, 'MasterSecondary', port); + public setSqlServerReadableSecondaryEndpoint(port: number, dnsName?: string) { + this.setEndpoint(this._bdcConfig.spec.resources.master.spec.endpoints, 'MasterSecondary', port, dnsName); } public get gatewayPort(): number { return this.getEndpointPort(this._bdcConfig.spec.resources.gateway.spec.endpoints, 'Knox', 30443); } - public set gatewayPort(port: number) { - this.setEndpointPort(this._bdcConfig.spec.resources.gateway.spec.endpoints, 'Knox', port); + public setGatewayEndpoint(port: number, dnsName?: string) { + this.setEndpoint(this._bdcConfig.spec.resources.gateway.spec.endpoints, 'Knox', port, dnsName); } public addSparkResource(replicas: number): void { @@ -220,8 +262,32 @@ export class BigDataClusterDeploymentProfile { } public get activeDirectorySupported(): boolean { - // TODO: Implement AD authentication - return false; + // The profiles that highlight the AD authentication feature will have a security secion in the control.json for the AD settings. + return 'security' in this._controlConfig; + } + + public setAuthenticationMode(mode: string): void { + // If basic authentication is picked, the security section must be removed + // otherwise azdata will throw validation error + if (mode === AuthenticationMode.Basic && 'security' in this._controlConfig) { + delete this._controlConfig.security; + } + } + + public setActiveDirectorySettings(adSettings: ActiveDirectorySettings): void { + this._controlConfig.security.ouDistinguishedName = adSettings.organizationalUnit; + this._controlConfig.security.dnsIpAddresses = this.splitByComma(adSettings.dnsIPAddresses); + this._controlConfig.security.domainControllerFullyQualifiedDns = this.splitByComma(adSettings.domainControllerFQDNs); + this._controlConfig.security.domainDnsName = adSettings.domainDNSName; + this._controlConfig.security.realm = adSettings.domainDNSName.toUpperCase(); + this._controlConfig.security.clusterAdmins = this.splitByComma(adSettings.clusterAdmins); + this._controlConfig.security.clusterUsers = this.splitByComma(adSettings.clusterUsers); + if (adSettings.appReaders) { + this._controlConfig.security.appReaders = this.splitByComma(adSettings.appReaders); + } + if (adSettings.appOwners) { + this._controlConfig.security.appOwners = this.splitByComma(adSettings.appOwners); + } } public getBdcJson(readable: boolean = true): string { @@ -249,16 +315,27 @@ export class BigDataClusterDeploymentProfile { return endpoint ? endpoint.port : defaultValue; } - private setEndpointPort(endpoints: ServiceEndpoint[], name: EndpointName, port: number): void { + private setEndpoint(endpoints: ServiceEndpoint[], name: EndpointName, port: number, dnsName?: string): void { const endpoint = endpoints.find(endpoint => endpoint.name === name); if (endpoint) { endpoint.port = port; + endpoint.dnsName = dnsName; } else { - endpoints.push({ + const newEndpoint: ServiceEndpoint = { name: name, serviceType: 'NodePort', port: port - }); + }; + // for newly added endpoint, we cannot have blank value for the dnsName, only set it if it is not empty + if (dnsName) { + newEndpoint.dnsName = dnsName; + } + endpoints.push(newEndpoint); } } + + private splitByComma(value: string): string[] { + // split by comma, then remove trailing spaces for each item and finally remove the empty values. + return value.split(',').map(v => v && v.trim()).filter(v => v !== '' && v !== undefined); + } } diff --git a/extensions/resource-deployment/src/services/notebookService.ts b/extensions/resource-deployment/src/services/notebookService.ts index 29d6d5e9f9..622c3ce810 100644 --- a/extensions/resource-deployment/src/services/notebookService.ts +++ b/extensions/resource-deployment/src/services/notebookService.ts @@ -10,10 +10,32 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { IPlatformService } from './platformService'; import { NotebookInfo } from '../interfaces'; +import { getErrorMessage, getDateTimeString } from '../utils'; const localize = nls.loadMessageBundle(); +export interface Notebook { + cells: NotebookCell[]; +} + +export interface NotebookCell { + cell_type: 'code'; + source: string[]; + metadata: {}; + outputs: string[]; + execution_count: number; +} + +export interface NotebookExecutionResult { + succeeded: boolean; + outputNotebook?: string; + errorMessage?: string; +} + export interface INotebookService { launchNotebook(notebook: string | NotebookInfo): Thenable; + launchNotebookWithContent(title: string, content: string): Thenable; + getNotebook(notebook: string | NotebookInfo): Promise; + executeNotebook(notebook: any, env: NodeJS.ProcessEnv): Promise; } export class NotebookService implements INotebookService { @@ -21,32 +43,89 @@ export class NotebookService implements INotebookService { constructor(private platformService: IPlatformService, private extensionPath: string) { } /** - * Copy the notebook to the user's home directory and launch the notebook from there. + * Launch notebook with file path * @param notebook the path of the notebook */ launchNotebook(notebook: string | NotebookInfo): Thenable { - const notebookPath = this.getNotebook(notebook); - const notebookFullPath = path.join(this.extensionPath, notebookPath); - return this.platformService.fileExists(notebookPath).then((notebookPathExists) => { - if (notebookPathExists) { - return this.showNotebookAsUntitled(notebookPath); - } else { - return this.platformService.fileExists(notebookFullPath).then(notebookFullPathExists => { - if (notebookFullPathExists) { - return this.showNotebookAsUntitled(notebookFullPath); - } else { - throw localize('resourceDeployment.notebookNotFound', "The notebook {0} does not exist", notebookPath); - } - }); - } + return this.getNotebookFullPath(notebook).then(notebookPath => { + return this.showNotebookAsUntitled(notebookPath); }); } + /** + * Launch notebook with file path + * @param title the title of the notebook + * @param content the notebook content + */ + launchNotebookWithContent(title: string, content: string): Thenable { + const uri: vscode.Uri = vscode.Uri.parse(`untitled:${title}`); + return azdata.nb.showNotebookDocument(uri, { + connectionProfile: undefined, + preview: false, + initialContent: content, + initialDirtyState: false + }); + } + + + async getNotebook(notebook: string | NotebookInfo): Promise { + const notebookPath = await this.getNotebookFullPath(notebook); + return JSON.parse(await this.platformService.readTextFile(notebookPath)); + } + + async executeNotebook(notebook: Notebook, env: NodeJS.ProcessEnv): Promise { + const content = JSON.stringify(notebook, undefined, 4); + const fileName = `nb-${getDateTimeString()}.ipynb`; + const workingDirectory = this.platformService.storagePath(); + const notebookFullPath = path.join(workingDirectory, fileName); + const outputFullPath = path.join(workingDirectory, `output-${fileName}`); + try { + await this.platformService.saveTextFile(content, notebookFullPath); + await this.platformService.runCommand(`azdata notebook run --path "${notebookFullPath}" --output-path "${workingDirectory}" --timeout -1`, + { + additionalEnvironmentVariables: env, + workingDirectory: workingDirectory + }); + return { + succeeded: true + }; + } + catch (error) { + const outputExists = await this.platformService.fileExists(outputFullPath); + return { + succeeded: false, + outputNotebook: outputExists ? await this.platformService.readTextFile(outputFullPath) : undefined, + errorMessage: getErrorMessage(error) + }; + } finally { + this.platformService.deleteFile(notebookFullPath); + this.platformService.deleteFile(outputFullPath); + } + } + + async getNotebookFullPath(notebook: string | NotebookInfo): Promise { + const notebookPath = this.getNotebookPath(notebook); + let notebookExists = await this.platformService.fileExists(notebookPath); + if (notebookExists) { + // this is for the scenarios when the provider is in a different extension, the full path will be passed in. + return notebookPath; + } + + // this is for the scenarios in this extension, the notebook paths are relative path. + const absolutePath = path.join(this.extensionPath, notebookPath); + notebookExists = await this.platformService.fileExists(absolutePath); + if (notebookExists) { + return absolutePath; + } else { + throw new Error(localize('resourceDeployment.notebookNotFound', "The notebook {0} does not exist", notebookPath)); + } + } + /** * get the notebook path for current platform * @param notebook the notebook path */ - getNotebook(notebook: string | NotebookInfo): string { + getNotebookPath(notebook: string | NotebookInfo): string { let notebookPath; if (notebook && !isString(notebook)) { const platform = this.platformService.platform(); diff --git a/extensions/resource-deployment/src/services/platformService.ts b/extensions/resource-deployment/src/services/platformService.ts index 05f18b792b..37c8a6cc42 100644 --- a/extensions/resource-deployment/src/services/platformService.ts +++ b/extensions/resource-deployment/src/services/platformService.ts @@ -7,6 +7,7 @@ import * as fs from 'fs'; import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as cp from 'child_process'; +import { getErrorMessage } from '../utils'; /** * Abstract of platform dependencies @@ -22,6 +23,8 @@ export interface IPlatformService { makeDirectory(path: string): Promise; readTextFile(filePath: string): Promise; runCommand(command: string, options?: CommandOptions): Promise; + saveTextFile(content: string, path: string): Promise; + deleteFile(path: string, ignoreError?: boolean): Promise; } export interface CommandOptions { @@ -91,4 +94,24 @@ export class PlatformService implements IPlatformService { }); }); } + + saveTextFile(content: string, path: string): Promise { + return fs.promises.writeFile(path, content, 'utf8'); + } + + async deleteFile(path: string, ignoreError: boolean = true): Promise { + try { + const exists = await this.fileExists(path); + if (exists) { + fs.promises.unlink(path); + } + } + catch (error) { + if (ignoreError) { + console.error('Error occured deleting file: ', getErrorMessage(error)); + } else { + throw error; + } + } + } } diff --git a/extensions/resource-deployment/src/services/tools/dockerTool.ts b/extensions/resource-deployment/src/services/tools/dockerTool.ts index db7a52c77e..248e19b3f3 100644 --- a/extensions/resource-deployment/src/services/tools/dockerTool.ts +++ b/extensions/resource-deployment/src/services/tools/dockerTool.ts @@ -29,7 +29,7 @@ export class DockerTool extends ToolBase { } get displayName(): string { - return localize('resourceDeployment.DockerDisplayName', 'Docker'); + return localize('resourceDeployment.DockerDisplayName', 'docker'); } get homePage(): string { diff --git a/extensions/resource-deployment/src/test/notebookService.test.ts b/extensions/resource-deployment/src/test/notebookService.test.ts index 9ab0e560a7..4aa3fed11a 100644 --- a/extensions/resource-deployment/src/test/notebookService.test.ts +++ b/extensions/resource-deployment/src/test/notebookService.test.ts @@ -17,13 +17,13 @@ suite('Notebook Service Tests', function (): void { const notebookService = new NotebookService(mockPlatformService.object, ''); const notebookInput = 'test-notebook.ipynb'; mockPlatformService.setup((service) => service.platform()).returns(() => { return 'win32'; }); - let returnValue = notebookService.getNotebook(notebookInput); + let returnValue = notebookService.getNotebookPath(notebookInput); assert.equal(returnValue, notebookInput, 'returned notebook name does not match expected value'); mockPlatformService.verify((service) => service.platform(), TypeMoq.Times.never()); mockPlatformService.reset(); mockPlatformService.setup((service) => service.platform()).returns(() => { return 'win32'; }); - returnValue = notebookService.getNotebook(''); + returnValue = notebookService.getNotebookPath(''); assert.equal(returnValue, '', 'returned notebook name does not match expected value is not an empty string'); mockPlatformService.verify((service) => service.platform(), TypeMoq.Times.never()); }); @@ -41,19 +41,19 @@ suite('Notebook Service Tests', function (): void { linux: notebookLinux }; mockPlatformService.setup((service) => service.platform()).returns(() => { return 'win32'; }); - let returnValue = notebookService.getNotebook(notebookInput); + let returnValue = notebookService.getNotebookPath(notebookInput); assert.equal(returnValue, notebookWin32, 'returned notebook name does not match expected value for win32 platform'); mockPlatformService.verify((service) => service.platform(), TypeMoq.Times.once()); mockPlatformService.reset(); mockPlatformService.setup((service) => service.platform()).returns(() => { return 'darwin'; }); - returnValue = notebookService.getNotebook(notebookInput); + returnValue = notebookService.getNotebookPath(notebookInput); assert.equal(returnValue, notebookDarwin, 'returned notebook name does not match expected value for darwin platform'); mockPlatformService.verify((service) => service.platform(), TypeMoq.Times.once()); mockPlatformService.reset(); mockPlatformService.setup((service) => service.platform()).returns(() => { return 'linux'; }); - returnValue = notebookService.getNotebook(notebookInput); + returnValue = notebookService.getNotebookPath(notebookInput); assert.equal(returnValue, notebookLinux, 'returned notebook name does not match expected value for linux platform'); mockPlatformService.verify((service) => service.platform(), TypeMoq.Times.once()); }); diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/constants.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/constants.ts index 079abce4b9..b759766b04 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/constants.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/constants.ts @@ -8,17 +8,20 @@ export const ClusterName_VariableName = 'AZDATA_NB_VAR_BDC_CLUSTER_NAME'; export const AdminUserName_VariableName = 'AZDATA_NB_VAR_BDC_CONTROLLER_USERNAME'; export const AdminPassword_VariableName = 'AZDATA_NB_VAR_BDC_ADMIN_PASSWORD'; export const AuthenticationMode_VariableName = 'AZDATA_NB_VAR_BDC_AUTHENTICATION_MODE'; -export const DistinguishedName_VariableName = 'AZDATA_NB_VAR_BDC_AD_DN'; -export const AdminPrincipals_VariableName = 'AZDATA_NB_VAR_BDC_AD_ADMIN_PRINCIPALS'; -export const UserPrincipals_VariableName = 'AZDATA_NB_VAR_BDC_AD_USER_PRINCIPALS'; -export const UpstreamIPAddresses_VariableName = 'AZDATA_NB_VAR_BDC_AD_UPSTREAM_IPADDRESSES'; -export const DnsName_VariableName = 'AZDATA_NB_VAR_BDC_AD_DNS_NAME'; +export const OrganizationalUnitDistinguishedName_VariableName = 'AZDATA_NB_VAR_BDC_AD_OUDN'; +export const ClusterAdmins_VariableName = 'AZDATA_NB_VAR_BDC_AD_CLUSTER_ADMINS'; +export const ClusterUsers_VariableName = 'AZDATA_NB_VAR_BDC_AD_CLUSTER_USERS'; +export const DomainDNSIPAddresses_VariableName = 'AZDATA_NB_VAR_BDC_AD_UPSTREAM_IPADDRESSES'; +export const DomainControllerFQDNs_VariableName = 'AZDATA_NB_VAR_BDC_AD_DC_FQDNs'; +export const DomainDNSName_VariableName = 'AZDATA_NB_VAR_BDC_AD_DOMAIN_DNS_NAME'; export const Realm_VariableName = 'AZDATA_NB_VAR_BDC_AD_REALM'; -export const AppOwnerPrincipals_VariableName = 'AZDATA_NB_VAR_AD_BDC_APP_OWNER_PRINCIPALS'; -export const AppReaderPrincipals_VariableName = 'AZDATA_NB_VAR_AD_BDC_APP_READER_PRINCIPALS'; +export const DomainServiceAccountUserName_VariableName = 'AZDATA_NB_VAR_BDC_AD_DOMAIN_SVC_USERNAME'; +export const DomainServiceAccountPassword_VariableName = 'AZDATA_NB_VAR_BDC_AD_DOMAIN_SVC_PASSWORD'; +export const AppOwners_VariableName = 'AZDATA_NB_VAR_AD_BDC_APP_OWNERS'; +export const AppReaders_VariableName = 'AZDATA_NB_VAR_AD_BDC_APP_READERS'; export const SubscriptionId_VariableName = 'AZDATA_NB_VAR_BDC_AZURE_SUBSCRIPTION'; export const ResourceGroup_VariableName = 'AZDATA_NB_VAR_BDC_RESOURCEGROUP_NAME'; -export const Region_VariableName = 'AZDATA_NB_VAR_BDC_AZURE_REGION'; +export const Location_VariableName = 'AZDATA_NB_VAR_BDC_AZURE_REGION'; export const AksName_VariableName = 'AZDATA_NB_VAR_BDC_AKS_NAME'; export const VMSize_VariableName = 'AZDATA_NB_VAR_BDC_AZURE_VM_SIZE'; export const VMCount_VariableName = 'AZDATA_NB_VAR_BDC_VM_COUNT'; @@ -57,4 +60,12 @@ export const GatewayDNSName_VariableName = 'AZDATA_NB_VAR_BDC_GATEWAY_DNS'; export const GateWayPort_VariableName = 'AZDATA_NB_VAR_BDC_GATEWAY_PORT'; export const ReadableSecondaryDNSName_VariableName = 'AZDATA_NB_VAR_BDC_READABLE_SECONDARY_DNS'; export const ReadableSecondaryPort_VariableName = 'AZDATA_NB_VAR_BDC_READABLE_SECONDARY_PORT'; -export const EnableHADR_VariableName = 'AZDATA_NB_VAR_BDC_ENABLE_HADR'; +export const ServiceProxyDNSName_VariableName = 'AZDATA_NB_VAR_BDC_SERVICEPROXY_DNS'; +export const ServiceProxyPort_VariableName = 'AZDATA_NB_VAR_BDC_SERVICEPROXY_PORT'; +export const AppServiceProxyDNSName_VariableName = 'AZDATA_NB_VAR_BDC_APPSERVICEPROXY_DNS'; +export const AppServiceProxyPort_VariableName = 'AZDATA_NB_VAR_BDC_APPSERVICEPROXY_PORT'; +export const DockerRepository_VariableName = 'AZDATA_NB_VAR_BDC_REPOSITORY'; +export const DockerRegistry_VariableName = 'AZDATA_NB_VAR_BDC_REGISTRY'; +export const DockerImageTag_VariableName = 'AZDATA_NB_VAR_BDC_DOCKER_IMAGE_TAG'; +export const DockerUsername_VariableName = 'AZDATA_NB_VAR_BDC_DOCKER_USERNAME'; +export const DockerPassword_VariableName = 'AZDATA_NB_VAR_BDC_DOCKER_PASSWORD'; diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizard.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizard.ts index 0555011caf..db835643cf 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizard.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizard.ts @@ -15,14 +15,20 @@ import { ClusterSettingsPage } from './pages/clusterSettingsPage'; import { ServiceSettingsPage } from './pages/serviceSettingsPage'; import { TargetClusterContextPage } from './pages/targetClusterPage'; import { IKubeService } from '../../services/kubeService'; -import { IAzdataService } from '../../services/azdataService'; +import { IAzdataService, BdcEndpoint } from '../../services/azdataService'; import { DeploymentProfilePage } from './pages/deploymentProfilePage'; import { INotebookService } from '../../services/notebookService'; -import { DeployClusterWizardModel } from './deployClusterWizardModel'; +import { getErrorMessage, getDateTimeString } from '../../utils'; +import { DeployClusterWizardModel, AuthenticationMode } from './deployClusterWizardModel'; import * as VariableNames from './constants'; +import * as os from 'os'; +import { join } from 'path'; +import * as fs from 'fs'; const localize = nls.loadMessageBundle(); export class DeployClusterWizard extends WizardBase { + private _saveConfigButton: azdata.window.Button; + private _scriptToNotebookButton: azdata.window.Button; public get kubeService(): IKubeService { return this._kubeService; @@ -36,8 +42,24 @@ export class DeployClusterWizard extends WizardBase this.saveConfigFiles())); + this.registerDisposable(this._scriptToNotebookButton.onClick(() => this.scriptToNotebook())); } public get deploymentType(): BdcDeploymentType { @@ -47,23 +69,79 @@ export class DeployClusterWizard extends WizardBase { - notebook.edit((editBuilder: azdata.nb.NotebookEditorEdit) => { - editBuilder.insertCell({ + const taskName = localize('resourceDeployment.DeployBDCTask', "Deploy SQL Server Big Data Cluster \"{0}\"", this.model.getStringValue(VariableNames.ClusterName_VariableName)); + azdata.tasks.startBackgroundOperation({ + displayName: taskName, + description: taskName, + isCancelable: false, + operation: async op => { + op.updateStatus(azdata.TaskStatus.InProgress); + const env: NodeJS.ProcessEnv = {}; + this.setEnvironmentVariables(env); + const notebook = await this.notebookService.getNotebook(this.wizardInfo.azdata_notebook); + notebook.cells.splice(3, 0, { cell_type: 'code', - source: this.model.getCodeCellContentForNotebook() - }, 7); - }); - }, (error) => { - vscode.window.showErrorMessage(error); + source: this.model.getCodeCellContentForNotebook(), + metadata: {}, + execution_count: 0, + outputs: [] + }); + const result = await this.notebookService.executeNotebook(notebook, env); + if (result.succeeded) { + op.updateStatus(azdata.TaskStatus.Succeeded); + const connectToMasterSql = localize('resourceDeployment.ConnectToMasterSQLServer', "Connect to Master SQL Server"); + const selectedOption = await vscode.window.showInformationMessage(localize('resourceDeployment.DeploymentSucceeded', "Successfully deployed SQL Server Big Data Cluster: {0}", + this.model.getStringValue(VariableNames.ClusterName_VariableName)), + connectToMasterSql); + if (selectedOption === connectToMasterSql) { + let endpoints: BdcEndpoint[]; + try { + endpoints = await this.azdataService.getEndpoints(this.model.getStringValue(VariableNames.ClusterName_VariableName)!, + this.model.getStringValue(VariableNames.AdminUserName_VariableName)!, + this.model.getStringValue(VariableNames.AdminPassword_VariableName)!); + } catch (error) { + vscode.window.showErrorMessage(localize('resourceDeployment.ErroRetrievingEndpoints', "Failed to retrieve the endpoint list. {0}{1}", os.EOL, getErrorMessage(error))); + return; + } + const sqlEndpoint = endpoints.find(endpoint => endpoint.name === 'sql-server-master'); + if (sqlEndpoint) { + vscode.commands.executeCommand('azdata.connect', { + serverName: sqlEndpoint.endpoint, + providerName: 'MSSQL', + authenticationType: 'SqlLogin', + userName: this.model.getStringValue(VariableNames.AdminUserName_VariableName)!, + password: this.model.getStringValue(VariableNames.AdminPassword_VariableName)! + }); + } else { + vscode.window.showErrorMessage(localize('resourceDeployment.NoSQLEndpointFound', "Master SQL Server endpoint is not found.")); + } + } + } else { + op.updateStatus(azdata.TaskStatus.Failed, result.errorMessage); + if (result.outputNotebook) { + const viewErrorDetail = localize('resourceDeployment.ViewErrorDetail', "View error detail"); + const selectedOption = await vscode.window.showErrorMessage(localize('resourceDeployment.DeployFailed', "Failed to deploy SQL Server Big Data Cluster \"{0}\".", + this.model.getStringValue(VariableNames.ClusterName_VariableName)), + viewErrorDetail); + if (selectedOption === viewErrorDetail) { + try { + this.notebookService.launchNotebookWithContent(`deploy-${getDateTimeString()}`, result.outputNotebook); + } catch (error) { + vscode.window.showErrorMessage(localize('resourceDeployment.FailedToOpenNotebook', "An error occured launching the output notebook. {1}{2}.", os.EOL, getErrorMessage(error))); + } + } + } else { + vscode.window.showErrorMessage(localize('resourceDeployment.DeployFailedNoOutputNotebook', "Failed to deploy SQL Server Big Data Cluster and no output notebook was generated.")); + } + } + } }); } @@ -100,6 +178,58 @@ export class DeployClusterWizard extends WizardBase { + const options: vscode.OpenDialogOptions = { + defaultUri: vscode.Uri.file(os.homedir()), + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: localize('deployCluster.SelectConfigFileFolder', "Save config files") + }; + const pathArray = await vscode.window.showOpenDialog(options); + if (pathArray && pathArray[0]) { + const targetFolder = pathArray[0].fsPath; + try { + const profile = this.model.createTargetProfile(); + await fs.promises.writeFile(join(targetFolder, 'bdc.json'), profile.getBdcJson()); + await fs.promises.writeFile(join(targetFolder, 'control.json'), profile.getControlJson()); + this.wizardObject.message = { + text: localize('deployCluster.SaveConfigFileSucceeded', "Config files saved to {0}", targetFolder), + level: azdata.window.MessageLevel.Information + }; + } + catch (error) { + this.wizardObject.message = { + text: error.message, + level: azdata.window.MessageLevel.Error + }; + } + } + } + + private scriptToNotebook(): void { + this.setEnvironmentVariables(process.env); + this.notebookService.launchNotebook(this.wizardInfo.notebook).then((notebook: azdata.nb.NotebookEditor) => { + notebook.edit((editBuilder: azdata.nb.NotebookEditorEdit) => { + // 5 is the position after the 'Set variables' cell in the deployment notebooks + editBuilder.insertCell({ + cell_type: 'code', + source: this.model.getCodeCellContentForNotebook() + }, 5); + }); + }, (error) => { + vscode.window.showErrorMessage(error); + }); + } + + private setEnvironmentVariables(env: NodeJS.ProcessEnv): void { + env[VariableNames.AdminPassword_VariableName] = this.model.getStringValue(VariableNames.AdminPassword_VariableName); + env[VariableNames.DockerPassword_VariableName] = this.model.getStringValue(VariableNames.DockerPassword_VariableName); + if (this.model.authenticationMode === AuthenticationMode.ActiveDirectory) { + env[VariableNames.DomainServiceAccountPassword_VariableName] = this.model.getStringValue(VariableNames.DomainServiceAccountPassword_VariableName); + } + } + static getTitle(type: BdcDeploymentType): string { switch (type) { case BdcDeploymentType.NewAKS: diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizardModel.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizardModel.ts index b8960946ea..e3d054cd3a 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizardModel.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizardModel.ts @@ -15,14 +15,6 @@ export class DeployClusterWizardModel extends Model { } public adAuthSupported: boolean = false; - public get hadrEnabled(): boolean { - return this.getBooleanValue(VariableNames.EnableHADR_VariableName); - } - - public set hadrEnabled(value: boolean) { - this.setPropertyValue(VariableNames.EnableHADR_VariableName, value); - } - public get authenticationMode(): string | undefined { return this.getStringValue(VariableNames.AuthenticationMode_VariableName); } @@ -71,6 +63,13 @@ export class DeployClusterWizardModel extends Model { const sourceBdcJson = Object.assign({}, this.selectedProfile!.bdcConfig); const sourceControlJson = Object.assign({}, this.selectedProfile!.controlConfig); const targetDeploymentProfile = new BigDataClusterDeploymentProfile('', sourceBdcJson, sourceControlJson); + // docker settings + targetDeploymentProfile.controlConfig.spec.docker = { + registry: this.getStringValue(VariableNames.DockerRegistry_VariableName), + repository: this.getStringValue(VariableNames.DockerRepository_VariableName), + imageTag: this.getStringValue(VariableNames.DockerImageTag_VariableName), + imagePullPolicy: 'Always' + }; // cluster name targetDeploymentProfile.clusterName = this.getStringValue(VariableNames.ClusterName_VariableName)!; // storage settings @@ -111,23 +110,37 @@ export class DeployClusterWizardModel extends Model { } targetDeploymentProfile.includeSpark = this.getBooleanValue(VariableNames.IncludeSpark_VariableName); - targetDeploymentProfile.hadrEnabled = this.getBooleanValue(VariableNames.EnableHADR_VariableName); - // port settings - targetDeploymentProfile.gatewayPort = this.getIntegerValue(VariableNames.GateWayPort_VariableName); - targetDeploymentProfile.sqlServerPort = this.getIntegerValue(VariableNames.SQLServerPort_VariableName); - targetDeploymentProfile.controllerPort = this.getIntegerValue(VariableNames.ControllerPort_VariableName); - targetDeploymentProfile.sqlServerReadableSecondaryPort = this.getIntegerValue(VariableNames.ReadableSecondaryPort_VariableName); + // endpoint settings + targetDeploymentProfile.setGatewayEndpoint(this.getIntegerValue(VariableNames.GateWayPort_VariableName), this.getStringValue(VariableNames.GatewayDNSName_VariableName)); + targetDeploymentProfile.setSqlServerEndpoint(this.getIntegerValue(VariableNames.SQLServerPort_VariableName), this.getStringValue(VariableNames.SQLServerDNSName_VariableName)); + targetDeploymentProfile.setControllerEndpoint(this.getIntegerValue(VariableNames.ControllerPort_VariableName), this.getStringValue(VariableNames.ControllerDNSName_VariableName)); + targetDeploymentProfile.setSqlServerReadableSecondaryEndpoint(this.getIntegerValue(VariableNames.ReadableSecondaryPort_VariableName), this.getStringValue(VariableNames.ReadableSecondaryDNSName_VariableName)); + targetDeploymentProfile.setServiceProxyEndpoint(this.getIntegerValue(VariableNames.ServiceProxyPort_VariableName), this.getStringValue(VariableNames.ServiceProxyDNSName_VariableName)); + targetDeploymentProfile.setAppServiceProxyEndpoint(this.getIntegerValue(VariableNames.AppServiceProxyPort_VariableName), this.getStringValue(VariableNames.AppServiceProxyDNSName_VariableName)); + targetDeploymentProfile.setAuthenticationMode(this.authenticationMode!); + if (this.authenticationMode === AuthenticationMode.ActiveDirectory) { + targetDeploymentProfile.setActiveDirectorySettings({ + organizationalUnit: this.getStringValue(VariableNames.OrganizationalUnitDistinguishedName_VariableName)!, + domainControllerFQDNs: this.getStringValue(VariableNames.DomainControllerFQDNs_VariableName)!, + domainDNSName: this.getStringValue(VariableNames.DomainDNSName_VariableName)!, + dnsIPAddresses: this.getStringValue(VariableNames.DomainDNSIPAddresses_VariableName)!, + clusterAdmins: this.getStringValue(VariableNames.ClusterAdmins_VariableName)!, + clusterUsers: this.getStringValue(VariableNames.ClusterUsers_VariableName)!, + appOwners: this.getStringValue(VariableNames.AppOwners_VariableName), + appReaders: this.getStringValue(VariableNames.AppReaders_VariableName) + }); + } return targetDeploymentProfile; } - public getCodeCellContentForNotebook(): string { + public getCodeCellContentForNotebook(): string[] { const profile = this.createTargetProfile(); const statements: string[] = []; if (this.deploymentTarget === BdcDeploymentType.NewAKS) { statements.push(`azure_subscription_id = '${this.getStringValue(VariableNames.SubscriptionId_VariableName, '')}'`); - statements.push(`azure_region = '${this.getStringValue(VariableNames.Region_VariableName)}'`); + statements.push(`azure_region = '${this.getStringValue(VariableNames.Location_VariableName)}'`); statements.push(`azure_resource_group = '${this.getStringValue(VariableNames.ResourceGroup_VariableName)}'`); statements.push(`azure_vm_size = '${this.getStringValue(VariableNames.VMSize_VariableName)}'`); statements.push(`azure_vm_count = '${this.getStringValue(VariableNames.VMCount_VariableName)}'`); @@ -137,12 +150,20 @@ export class DeployClusterWizardModel extends Model { statements.push(`mssql_cluster_context = '${this.getStringValue(VariableNames.ClusterContext_VariableName)}'`); statements.push('os.environ["KUBECONFIG"] = mssql_kube_config_path'); } + if (this.authenticationMode === AuthenticationMode.ActiveDirectory) { + statements.push(`mssql_domain_service_account_username = '${this.escapeForNotebookCodeCell(this.getStringValue(VariableNames.DomainServiceAccountUserName_VariableName)!)}'`); + } statements.push(`mssql_cluster_name = '${this.getStringValue(VariableNames.ClusterName_VariableName)}'`); - statements.push(`mssql_controller_username = '${this.getStringValue(VariableNames.AdminUserName_VariableName)}'`); + statements.push(`mssql_username = '${this.getStringValue(VariableNames.AdminUserName_VariableName)}'`); + statements.push(`mssql_auth_mode = '${this.authenticationMode}'`); statements.push(`bdc_json = '${profile.getBdcJson(false)}'`); statements.push(`control_json = '${profile.getControlJson(false)}'`); + if (this.getStringValue(VariableNames.DockerUsername_VariableName) && this.getStringValue(VariableNames.DockerPassword_VariableName)) { + statements.push(`os.environ["DOCKER_USERNAME"] = '${this.getStringValue(VariableNames.DockerUsername_VariableName)}'`); + statements.push(`os.environ["DOCKER_PASSWORD"] = os.environ["${VariableNames.DockerPassword_VariableName}"]`); + } statements.push(`print('Variables have been set successfully.')`); - return statements.join(EOL); + return statements.map(line => line + EOL); } private escapeForNotebookCodeCell(original: string): string { diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts index c8256b8997..80059418e3 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts @@ -9,8 +9,8 @@ import * as nls from 'vscode-nls'; import { DeployClusterWizard } from '../deployClusterWizard'; import { SectionInfo, FieldType, LabelPosition } from '../../../interfaces'; import { WizardPageBase } from '../../wizardPageBase'; -import { createSection, InputComponents, setModelValues, Validator } from '../../modelViewUtils'; -import { SubscriptionId_VariableName, ResourceGroup_VariableName, Region_VariableName, AksName_VariableName, VMCount_VariableName, VMSize_VariableName } from '../constants'; +import { createSection, InputComponents, setModelValues, Validator, getDropdownComponent, MissingRequiredInformationErrorMessage } from '../../modelViewUtils'; +import { SubscriptionId_VariableName, ResourceGroup_VariableName, Location_VariableName, AksName_VariableName, VMCount_VariableName, VMSize_VariableName } from '../constants'; const localize = nls.loadMessageBundle(); export class AzureSettingsPage extends WizardPageBase { @@ -26,8 +26,9 @@ export class AzureSettingsPage extends WizardPageBase { const azureSection: SectionInfo = { title: '', labelPosition: LabelPosition.Left, - fields: [ - { + spaceBetweenFields: '5px', + rows: [{ + fields: [{ type: FieldType.Text, label: localize('deployCluster.SubscriptionField', "Subscription id"), required: false, @@ -35,42 +36,100 @@ export class AzureSettingsPage extends WizardPageBase { placeHolder: localize('deployCluster.SubscriptionPlaceholder', "Use my default Azure subscription"), description: localize('deployCluster.SubscriptionDescription', "The default subscription will be used if you leave this field blank.") }, { + type: FieldType.ReadonlyText, + label: '', + labelWidth: '0px', + defaultValue: localize('deployCluster.SubscriptionHelpText', "{0}"), + links: [ + { + text: localize('deployCluster.SubscriptionHelpLink', "View available Azure subscriptions"), + url: 'https://portal.azure.com/#blade/Microsoft_Azure_Billing/SubscriptionsBlade' + } + ] + }] + }, { + fields: [{ type: FieldType.DateTimeText, label: localize('deployCluster.ResourceGroupName', "New resource group name"), required: true, variableName: ResourceGroup_VariableName, defaultValue: 'mssql-' - }, { - type: FieldType.Text, - label: localize('deployCluster.Region', "Region"), + }] + }, { + fields: [{ + type: FieldType.Options, + label: localize('deployCluster.Location', "Location"), required: true, - variableName: Region_VariableName, - defaultValue: 'eastus' + variableName: Location_VariableName, + defaultValue: 'eastus', + editable: true, + // The options are not localized because this is an editable dropdown, + // It would cause confusion to user about what value to type in, if they type in the localized value, we don't know how to process. + options: [ + 'centralus', + 'eastus', + 'eastus2', + 'northcentralus', + 'southcentralus', + 'westus', + 'westus2', + 'canadacentral', + 'canadaeast' + ] }, { + type: FieldType.ReadonlyText, + label: '', + labelWidth: '0px', + defaultValue: localize('deployCluster.LocationHelpText', "{0}"), + links: [ + { + text: localize('deployCluster.AzureLocationHelpLink', "View available Azure locations"), + url: 'https://azure.microsoft.com/global-infrastructure/services/?products=kubernetes-service' + } + ] + }] + }, { + fields: [{ type: FieldType.DateTimeText, label: localize('deployCluster.AksName', "AKS cluster name"), required: true, variableName: AksName_VariableName, defaultValue: 'mssql-', - }, { - type: FieldType.Number, - label: localize('deployCluster.VMCount', "VM count"), - required: true, - variableName: VMCount_VariableName, - defaultValue: '5', - min: 1, - max: 999 - }, { + }] + }, { + fields: [ + { + type: FieldType.Number, + label: localize('deployCluster.VMCount', "VM count"), + required: true, + variableName: VMCount_VariableName, + defaultValue: '5', + min: 1, + max: 999 + } + ] + }, { + fields: [{ type: FieldType.Text, label: localize('deployCluster.VMSize', "VM size"), required: true, variableName: VMSize_VariableName, - defaultValue: 'Standard_E4s_v3' - } - ] + defaultValue: 'Standard_E8s_v3' + }, { + type: FieldType.ReadonlyText, + label: '', + labelWidth: '0px', + defaultValue: localize('deployCluster.VMSizeHelpText', "{0}"), + links: [ + { + text: localize('deployCluster.VMSizeHelpLink', "View available VM sizes"), + url: 'https://docs.microsoft.com/azure/virtual-machines/linux/sizes' + } + ] + }] + }] }; this.pageObject.registerContent((view: azdata.ModelView) => { - const azureGroup = createSection({ sectionInfo: azureSection, view: view, @@ -101,7 +160,28 @@ export class AzureSettingsPage extends WizardPageBase { }); } + public onEnter(): void { + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { + this.wizard.wizardObject.message = { text: '' }; + if (pcInfo.newPage > pcInfo.lastPage) { + const location = getDropdownComponent(Location_VariableName, this.inputComponents).value; + if (!location) { + this.wizard.wizardObject.message = { + text: MissingRequiredInformationErrorMessage, + level: azdata.window.MessageLevel.Error + }; + } + return !!location; + } else { + return true; + } + }); + } + public onLeave(): void { + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { + return true; + }); setModelValues(this.inputComponents, this.wizard.model); } } diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts index 6e874db672..5739aeaca0 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts @@ -18,6 +18,8 @@ const localize = nls.loadMessageBundle(); const ConfirmPasswordName = 'ConfirmPassword'; export class ClusterSettingsPage extends WizardPageBase { private inputComponents: InputComponents = {}; + private activeDirectorySection!: azdata.FormComponent; + private formBuilder!: azdata.FormBuilder; constructor(wizard: DeployClusterWizard) { super(localize('deployCluster.ClusterSettingsPageTitle', "Cluster settings"), @@ -39,11 +41,12 @@ export class ClusterSettingsPage extends WizardPageBase { useCustomValidator: true }, { type: FieldType.Text, - label: localize('deployCluster.ControllerUsername', "Controller username"), + label: localize('deployCluster.AdminUsername', "Admin username"), required: true, variableName: VariableNames.AdminUserName_VariableName, defaultValue: 'admin', - useCustomValidator: true + useCustomValidator: true, + description: localize('deployCluster.AdminUsernameDescription', "This username will be used for controller and SQL Server. Username for the gateway will be root.") }, { type: FieldType.Password, label: localize('deployCluster.AdminPassword', "Password"), @@ -51,7 +54,7 @@ export class ClusterSettingsPage extends WizardPageBase { variableName: VariableNames.AdminPassword_VariableName, defaultValue: '', useCustomValidator: true, - description: localize('deployCluster.AdminPasswordDescription', "You can also use this password to access SQL Server and gateway.") + description: localize('deployCluster.AdminPasswordDescription', "This password can be used to access the controller, SQL Server and gateway.") }, { type: FieldType.Password, label: localize('deployCluster.ConfirmPassword', "Confirm password"), @@ -80,58 +83,118 @@ export class ClusterSettingsPage extends WizardPageBase { ] }; + const dockerSection: SectionInfo = { + labelPosition: LabelPosition.Left, + collapsed: true, + collapsible: true, + title: localize('deployCluster.DockerSettings', "Docker settings"), + fields: [ + { + type: FieldType.Text, + label: localize('deployCluster.DockerRegistry', "Registry"), + required: true, + variableName: VariableNames.DockerRegistry_VariableName + }, { + type: FieldType.Text, + label: localize('deployCluster.DockerRepository', "Repository"), + required: true, + variableName: VariableNames.DockerRepository_VariableName + }, { + type: FieldType.Text, + label: localize('deployCluster.DockerImageTag', "Image tag"), + required: true, + variableName: VariableNames.DockerImageTag_VariableName + }, { + type: FieldType.Text, + label: localize('deployCluster.DockerUsername', "Username"), + required: false, + variableName: VariableNames.DockerUsername_VariableName + }, { + type: FieldType.Text, + label: localize('deployCluster.DockerPassword', "Password"), + required: false, + variableName: VariableNames.DockerPassword_VariableName + } + ] + }; + const activeDirectorySection: SectionInfo = { labelPosition: LabelPosition.Left, title: localize('deployCluster.ActiveDirectorySettings', "Active Directory settings"), fields: [ { type: FieldType.Text, - label: localize('deployCluster.DistinguishedName', "Distinguished name"), + label: localize('deployCluster.OuDistinguishedName', "Organizational unit"), required: true, - variableName: VariableNames.DistinguishedName_VariableName, + variableName: VariableNames.OrganizationalUnitDistinguishedName_VariableName, + useCustomValidator: true, + description: localize('deployCluster.OuDistinguishedNameDescription', "Distinguished name for the organizational unit. For example: OU=bdc,DC=contoso,DC=com.") + }, { + type: FieldType.Text, + label: localize('deployCluster.DomainControllerFQDNs', "Domain controller FQDNs"), + required: true, + variableName: VariableNames.DomainControllerFQDNs_VariableName, + useCustomValidator: true, + placeHolder: localize('deployCluster.DomainControllerFQDNsPlaceHolder', "Use comma to separate the values."), + description: localize('deployCluster.DomainControllerFQDNDescription', "Fully qualified domain names for the domain controller. For example: DC1.CONTOSO.COM. Use comma to separate multiple FQDNs.") + }, { + type: FieldType.Text, + label: localize('deployCluster.DomainDNSIPAddresses', "Domain DNS IP addresses"), + required: true, + variableName: VariableNames.DomainDNSIPAddresses_VariableName, + useCustomValidator: true, + placeHolder: localize('deployCluster.DomainDNSIPAddressesPlaceHolder', "Use comma to separate the values."), + description: localize('deployCluster.DomainDNSIPAddressesDescription', "Domain DNS servers' IP Addresses. Use comma to separate multiple IP addresses.") + }, { + type: FieldType.Text, + label: localize('deployCluster.DomainDNSName', "Domain DNS name"), + required: true, + variableName: VariableNames.DomainDNSName_VariableName, useCustomValidator: true }, { type: FieldType.Text, - label: localize('deployCluster.AdminPrincipals', "Admin principals"), + label: localize('deployCluster.ClusterAdmins', "Cluster admin group"), required: true, - variableName: VariableNames.AdminPrincipals_VariableName, + variableName: VariableNames.ClusterAdmins_VariableName, + useCustomValidator: true, + description: localize('deployCluster.ClusterAdminsDescription', "The Active Directory group for cluster admin.") + }, { + type: FieldType.Text, + label: localize('deployCluster.ClusterUsers', "Cluster users"), + required: true, + variableName: VariableNames.ClusterUsers_VariableName, + useCustomValidator: true, + placeHolder: localize('deployCluster.ClusterUsersPlaceHolder', "Use comma to separate the values."), + description: localize('deployCluster.ClusterUsersDescription', "The Active Directory users/groups with cluster users role. Use comma to separate multiple users/groups.") + }, { + type: FieldType.Text, + label: localize('deployCluster.DomainServiceAccountUserName', "Service account username"), + required: true, + variableName: VariableNames.DomainServiceAccountUserName_VariableName, + useCustomValidator: true, + description: localize('deployCluster.DomainServiceAccountUserNameDescription', "Domain service account for Big Data Cluster") + }, { + type: FieldType.Password, + label: localize('deployCluster.DomainServiceAccountPassword', "Service account password"), + required: true, + variableName: VariableNames.DomainServiceAccountPassword_VariableName, useCustomValidator: true }, { type: FieldType.Text, - label: localize('deployCluster.UserPrincipals', "User principals"), - required: true, - variableName: VariableNames.UserPrincipals_VariableName, - useCustomValidator: true + label: localize('deployCluster.AppOwers', "App owners"), + required: false, + variableName: VariableNames.AppOwners_VariableName, + useCustomValidator: true, + placeHolder: localize('deployCluster.AppOwnersPlaceHolder', "Use comma to separate the values."), + description: localize('deployCluster.AppOwnersDescription', "The Active Directory users or groups with app owners role. Use comma to separate multiple users/groups.") }, { type: FieldType.Text, - label: localize('deployCluster.UpstreamIPAddresses', "Upstream IP Addresses"), - required: true, - variableName: VariableNames.UpstreamIPAddresses_VariableName, - useCustomValidator: true - }, { - type: FieldType.Text, - label: localize('deployCluster.DNSName', "DNS name"), - required: true, - variableName: VariableNames.DnsName_VariableName, - useCustomValidator: true - }, { - type: FieldType.Text, - label: localize('deployCluster.Realm', "Realm"), - required: true, - variableName: VariableNames.Realm_VariableName, - useCustomValidator: true - }, { - type: FieldType.Text, - label: localize('deployCluster.AppOnwerPrincipals', "App owner principals"), - required: true, - variableName: VariableNames.AppOwnerPrincipals_VariableName, - useCustomValidator: true - }, { - type: FieldType.Text, - label: localize('deployCluster.AppReaderPrincipals', "App reader principals"), - required: true, - variableName: VariableNames.AppReaderPrincipals_VariableName, - useCustomValidator: true + label: localize('deployCluster.AppReaders', "App readers"), + required: false, + variableName: VariableNames.AppReaders_VariableName, + useCustomValidator: true, + placeHolder: localize('deployCluster.AppReadersPlaceHolder', "Use comma to separate the values."), + description: localize('deployCluster.AppReadersDescription', "The Active Directory users or groups of app readers. Use comma as separator them if there are multiple users/groups.") } ] }; @@ -164,11 +227,26 @@ export class ClusterSettingsPage extends WizardPageBase { self.validators.push(validator); } }); + const dockerSettingsGroup = createSection({ + view: view, + container: self.wizard.wizardObject, + sectionInfo: dockerSection, + onNewDisposableCreated: (disposable: vscode.Disposable): void => { + self.wizard.registerDisposable(disposable); + }, + onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => { + self.inputComponents[name] = component; + }, + onNewValidatorCreated: (validator: Validator): void => { + self.validators.push(validator); + } + }); const basicSettingsFormItem = { title: '', component: basicSettingsGroup }; - const activeDirectoryFormItem = { title: '', component: activeDirectorySettingsGroup }; + const dockerSettingsFormItem = { title: '', component: dockerSettingsGroup }; + this.activeDirectorySection = { title: '', component: activeDirectorySettingsGroup }; const authModeDropdown = this.inputComponents[VariableNames.AuthenticationMode_VariableName]; - const formBuilder = view.modelBuilder.formContainer().withFormItems( - [basicSettingsFormItem], + this.formBuilder = view.modelBuilder.formContainer().withFormItems( + [basicSettingsFormItem, dockerSettingsFormItem], { horizontal: false, componentWidth: '100%' @@ -176,76 +254,99 @@ export class ClusterSettingsPage extends WizardPageBase { ); this.wizard.registerDisposable(authModeDropdown.onValueChanged(() => { const isBasicAuthMode = (authModeDropdown.value).name === 'basic'; - if (isBasicAuthMode) { - formBuilder.removeFormItem(activeDirectoryFormItem); + this.formBuilder.removeFormItem(this.activeDirectorySection); } else { - formBuilder.insertFormItem(activeDirectoryFormItem); + this.formBuilder.insertFormItem(this.activeDirectorySection); } })); - - const form = formBuilder.withLayout({ width: '100%' }).component(); + const form = this.formBuilder.withLayout({ width: '100%' }).component(); return view.initializeModel(form); }); } public onLeave() { setModelValues(this.inputComponents, this.wizard.model); + if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) { + const variableDNSPrefixMapping: { [s: string]: string } = {}; + variableDNSPrefixMapping[VariableNames.AppServiceProxyDNSName_VariableName] = 'bdc-appproxy'; + variableDNSPrefixMapping[VariableNames.ControllerDNSName_VariableName] = 'bdc-control'; + variableDNSPrefixMapping[VariableNames.GatewayDNSName_VariableName] = 'bdc-gateway'; + variableDNSPrefixMapping[VariableNames.ReadableSecondaryDNSName_VariableName] = 'bdc-sqlread'; + variableDNSPrefixMapping[VariableNames.SQLServerDNSName_VariableName] = 'bdc-sql'; + variableDNSPrefixMapping[VariableNames.ServiceProxyDNSName_VariableName] = 'bdc-proxy'; + + Object.keys(variableDNSPrefixMapping).forEach((variableName: string) => { + if (!this.wizard.model.getStringValue(variableName)) { + this.wizard.model.setPropertyValue(variableName, `${variableDNSPrefixMapping[variableName]}.${this.wizard.model.getStringValue(VariableNames.DomainDNSName_VariableName)}`); + } + }); + } this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { return true; }); } public onEnter() { + getInputBoxComponent(VariableNames.DockerRegistry_VariableName, this.inputComponents).value = this.wizard.model.getStringValue(VariableNames.DockerRegistry_VariableName); + getInputBoxComponent(VariableNames.DockerRepository_VariableName, this.inputComponents).value = this.wizard.model.getStringValue(VariableNames.DockerRepository_VariableName); + getInputBoxComponent(VariableNames.DockerImageTag_VariableName, this.inputComponents).value = this.wizard.model.getStringValue(VariableNames.DockerImageTag_VariableName); const authModeDropdown = this.inputComponents[VariableNames.AuthenticationMode_VariableName]; if (authModeDropdown) { authModeDropdown.enabled = this.wizard.model.adAuthSupported; - } - - this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { - this.wizard.wizardObject.message = { text: '' }; - if (pcInfo.newPage > pcInfo.lastPage) { - const messages: string[] = []; - const authMode = typeof authModeDropdown.value === 'string' ? authModeDropdown.value : authModeDropdown.value!.name; - const requiredFieldsFilled: boolean = !isInputBoxEmpty(getInputBoxComponent(VariableNames.ClusterName_VariableName, this.inputComponents)) - && !isInputBoxEmpty(getInputBoxComponent(VariableNames.AdminUserName_VariableName, this.inputComponents)) - && !isInputBoxEmpty(getInputBoxComponent(VariableNames.AdminPassword_VariableName, this.inputComponents)) - && !isInputBoxEmpty(getInputBoxComponent(ConfirmPasswordName, this.inputComponents)) - && (!(authMode === AuthenticationMode.ActiveDirectory) || ( - !isInputBoxEmpty(getInputBoxComponent(VariableNames.DistinguishedName_VariableName, this.inputComponents)) - && !isInputBoxEmpty(getInputBoxComponent(VariableNames.AdminPrincipals_VariableName, this.inputComponents)) - && !isInputBoxEmpty(getInputBoxComponent(VariableNames.UserPrincipals_VariableName, this.inputComponents)) - && !isInputBoxEmpty(getInputBoxComponent(VariableNames.UpstreamIPAddresses_VariableName, this.inputComponents)) - && !isInputBoxEmpty(getInputBoxComponent(VariableNames.DnsName_VariableName, this.inputComponents)) - && !isInputBoxEmpty(getInputBoxComponent(VariableNames.Realm_VariableName, this.inputComponents)) - && !isInputBoxEmpty(getInputBoxComponent(VariableNames.AppOwnerPrincipals_VariableName, this.inputComponents)) - && !isInputBoxEmpty(getInputBoxComponent(VariableNames.AppReaderPrincipals_VariableName, this.inputComponents)))); - if (!requiredFieldsFilled) { - messages.push(MissingRequiredInformationErrorMessage); - } - - if (!isInputBoxEmpty(getInputBoxComponent(VariableNames.AdminPassword_VariableName, this.inputComponents)) - && !isInputBoxEmpty(getInputBoxComponent(ConfirmPasswordName, this.inputComponents))) { - const password = getInputBoxComponent(VariableNames.AdminPassword_VariableName, this.inputComponents).value!; - const confirmPassword = getInputBoxComponent(ConfirmPasswordName, this.inputComponents).value!; - if (password !== confirmPassword) { - messages.push(getPasswordMismatchMessage(localize('deployCluster.AdminPasswordField', "Password"))); - } - if (!isValidSQLPassword(password)) { - messages.push(getInvalidSQLPasswordMessage(localize('deployCluster.AdminPasswordField', "Password"))); - } - } - - if (messages.length > 0) { - this.wizard.wizardObject.message = { - text: messages.length === 1 ? messages[0] : localize('deployCluster.ValidationError', "There are some errors on this page, click 'Show Details' to view the errors."), - description: messages.length === 1 ? undefined : messages.join(EOL), - level: azdata.window.MessageLevel.Error - }; - } - return messages.length === 0; + const adAuthSelected = (authModeDropdown.value).name === 'ad'; + if (!this.wizard.model.adAuthSupported && adAuthSelected) { + this.formBuilder.removeFormItem(this.activeDirectorySection); + authModeDropdown.value = { + name: AuthenticationMode.Basic, + displayName: localize('deployCluster.AuthenticationMode.Basic', "Basic") + }; } - return true; - }); + + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { + this.wizard.wizardObject.message = { text: '' }; + if (pcInfo.newPage > pcInfo.lastPage) { + const messages: string[] = []; + const authMode = typeof authModeDropdown.value === 'string' ? authModeDropdown.value : authModeDropdown.value!.name; + const requiredFieldsFilled: boolean = !isInputBoxEmpty(getInputBoxComponent(VariableNames.ClusterName_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.AdminUserName_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.AdminPassword_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(ConfirmPasswordName, this.inputComponents)) + && (!(authMode === AuthenticationMode.ActiveDirectory) || ( + !isInputBoxEmpty(getInputBoxComponent(VariableNames.OrganizationalUnitDistinguishedName_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.DomainControllerFQDNs_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.ClusterAdmins_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.ClusterUsers_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.DomainDNSIPAddresses_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.DomainDNSName_VariableName, this.inputComponents)))); + if (!requiredFieldsFilled) { + messages.push(MissingRequiredInformationErrorMessage); + } + + if (!isInputBoxEmpty(getInputBoxComponent(VariableNames.AdminUserName_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.AdminPassword_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(ConfirmPasswordName, this.inputComponents))) { + const password = getInputBoxComponent(VariableNames.AdminPassword_VariableName, this.inputComponents).value!; + const confirmPassword = getInputBoxComponent(ConfirmPasswordName, this.inputComponents).value!; + if (password !== confirmPassword) { + messages.push(getPasswordMismatchMessage(localize('deployCluster.AdminPasswordField', "Password"))); + } + if (!isValidSQLPassword(password, getInputBoxComponent(VariableNames.AdminUserName_VariableName, this.inputComponents).value!)) { + messages.push(getInvalidSQLPasswordMessage(localize('deployCluster.AdminPasswordField', "Password"))); + } + } + + if (messages.length > 0) { + this.wizard.wizardObject.message = { + text: messages.length === 1 ? messages[0] : localize('deployCluster.ValidationError', "There are some errors on this page, click 'Show Details' to view the errors."), + description: messages.length === 1 ? undefined : messages.join(EOL), + level: azdata.window.MessageLevel.Error + }; + } + return messages.length === 0; + } + return true; + }); + } } } diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/deploymentProfilePage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/deploymentProfilePage.ts index 37c74771c0..e02105190a 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/deploymentProfilePage.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/deploymentProfilePage.ts @@ -81,31 +81,36 @@ export class DeploymentProfilePage extends WizardPageBase { }, { label: '' // line separator }, { - label: localize('deployCluster.defaultDataStorage', "Data storage size (GB)"), + label: localize('deployCluster.storageSize', "Storage size"), + value: localize('deployCluster.gbPerInstance', "GB per Instance"), + fontWeight: 'bold' + }, { + label: localize('deployCluster.defaultDataStorage', "Data storage"), value: profile.controllerDataStorageSize.toString() }, { - label: localize('deployCluster.defaultLogStorage', "Log storage size (GB)"), + label: localize('deployCluster.defaultLogStorage', "Log storage"), value: profile.controllerLogsStorageSize.toString() }, { label: '' // line separator - } - ]; + }, { + label: localize('deployCluster.features', "Features"), + value: '', + fontWeight: 'bold' + }, { + label: localize('deployCluster.basicAuthentication', "Basic authentication"), + value: '' + }]; if (profile.activeDirectorySupported) { descriptions.push({ label: localize('deployCluster.activeDirectoryAuthentication', "Active Directory authentication"), - value: '✅' - }); - } else { - descriptions.push({ - label: localize('deployCluster.basicAuthentication', "Basic authentication"), - value: '✅' + value: '' }); } - if (profile.hadrEnabled) { + if (profile.sqlServerReplicas > 1) { descriptions.push({ label: localize('deployCluster.hadr', "High Availability"), - value: '✅' + value: '' }); } @@ -114,7 +119,7 @@ export class DeploymentProfilePage extends WizardPageBase { label: profile.profileName, descriptions: descriptions, width: '240px', - height: '300px', + height: '320px', }).component(); this._cards.push(card); this.wizard.registerDisposable(card.onCardSelectedChanged(() => { @@ -150,20 +155,24 @@ export class DeploymentProfilePage extends WizardPageBase { this.wizard.model.setPropertyValue(VariableNames.ZooKeeperScale_VariableName, selectedProfile.zooKeeperReplicas); this.wizard.model.setPropertyValue(VariableNames.ControllerDataStorageSize_VariableName, selectedProfile.controllerDataStorageSize); this.wizard.model.setPropertyValue(VariableNames.ControllerLogsStorageSize_VariableName, selectedProfile.controllerLogsStorageSize); - this.wizard.model.setPropertyValue(VariableNames.EnableHADR_VariableName, selectedProfile.hadrEnabled); this.wizard.model.setPropertyValue(VariableNames.SQLServerPort_VariableName, selectedProfile.sqlServerPort); this.wizard.model.setPropertyValue(VariableNames.GateWayPort_VariableName, selectedProfile.gatewayPort); this.wizard.model.setPropertyValue(VariableNames.ControllerPort_VariableName, selectedProfile.controllerPort); + this.wizard.model.setPropertyValue(VariableNames.ServiceProxyPort_VariableName, selectedProfile.serviceProxyPort); + this.wizard.model.setPropertyValue(VariableNames.AppServiceProxyPort_VariableName, selectedProfile.appServiceProxyPort); this.wizard.model.setPropertyValue(VariableNames.IncludeSpark_VariableName, selectedProfile.includeSpark); this.wizard.model.setPropertyValue(VariableNames.ControllerDataStorageClassName_VariableName, selectedProfile.controllerDataStorageClass); this.wizard.model.setPropertyValue(VariableNames.ControllerLogsStorageClassName_VariableName, selectedProfile.controllerLogsStorageClass); this.wizard.model.setPropertyValue(VariableNames.ReadableSecondaryPort_VariableName, selectedProfile.sqlServerReadableSecondaryPort); + this.wizard.model.setPropertyValue(VariableNames.DockerRegistry_VariableName, selectedProfile.registry); + this.wizard.model.setPropertyValue(VariableNames.DockerRepository_VariableName, selectedProfile.repository); + this.wizard.model.setPropertyValue(VariableNames.DockerImageTag_VariableName, selectedProfile.imageTag); this.wizard.model.adAuthSupported = selectedProfile.activeDirectorySupported; this.wizard.model.selectedProfile = selectedProfile; } private loadCards(): Promise { - return this.wizard.azdataService.getDeploymentProfiles().then((profiles: BigDataClusterDeploymentProfile[]) => { + return this.wizard.azdataService.getDeploymentProfiles(this.wizard.deploymentType).then((profiles: BigDataClusterDeploymentProfile[]) => { const defaultProfile: string = this.getDefaultProfile(); profiles.forEach(profile => { diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts index f4cf77def1..458b5969c8 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts @@ -13,9 +13,9 @@ import * as VariableNames from '../constants'; import { AuthenticationMode } from '../deployClusterWizardModel'; const localize = nls.loadMessageBundle(); -const PortInputWidth = '100px'; +const NumberInputWidth = '100px'; const inputWidth = '180px'; -const labelWidth = '150px'; +const labelWidth = '200px'; const spaceBetweenFields = '5px'; export class ServiceSettingsPage extends WizardPageBase { @@ -32,6 +32,12 @@ export class ServiceSettingsPage extends WizardPageBase { private gatewayDNSInput!: azdata.InputBoxComponent; private gatewayPortInput!: azdata.InputBoxComponent; private gatewayEndpointRow!: azdata.FlexContainer; + private serviceProxyDNSInput!: azdata.InputBoxComponent; + private serviceProxyPortInput!: azdata.InputBoxComponent; + private serviceProxyEndpointRow!: azdata.FlexContainer; + private appServiceProxyDNSInput!: azdata.InputBoxComponent; + private appServiceProxyPortInput!: azdata.InputBoxComponent; + private appServiceProxyEndpointRow!: azdata.FlexContainer; private readableSecondaryDNSInput!: azdata.InputBoxComponent; private readableSecondaryPortInput!: azdata.InputBoxComponent; private readableSecondaryEndpointRow!: azdata.FlexContainer; @@ -39,7 +45,10 @@ export class ServiceSettingsPage extends WizardPageBase { private controllerNameLabel!: azdata.TextComponent; private SqlServerNameLabel!: azdata.TextComponent; private gatewayNameLabel!: azdata.TextComponent; + private serviceProxyNameLabel!: azdata.TextComponent; + private appServiceProxyNameLabel!: azdata.TextComponent; private readableSecondaryNameLabel!: azdata.TextComponent; + private endpointSection!: azdata.GroupContainer; constructor(wizard: DeployClusterWizard) { super(localize('deployCluster.ServiceSettingsPageTitle', "Service settings"), '', wizard); @@ -48,37 +57,51 @@ export class ServiceSettingsPage extends WizardPageBase { const scaleSectionInfo: SectionInfo = { title: localize('deployCluster.scaleSectionTitle', "Scale settings"), labelWidth: labelWidth, - inputWidth: inputWidth, - spaceBetweenFields: spaceBetweenFields, + inputWidth: NumberInputWidth, + spaceBetweenFields: '40px', rows: [{ - fields: [ - { - type: FieldType.Number, - label: localize('deployCluster.ComputeText', "Compute"), - min: 1, - max: 100, - defaultValue: '1', - useCustomValidator: true, - required: true, - variableName: VariableNames.ComputePoolScale_VariableName, - } - ] + fields: [{ + type: FieldType.Options, + label: localize('deployCluster.MasterSqlServerInstances', "SQL Server master instances"), + options: ['1', '3', '4', '5', '6', '7', '8', '9'], + defaultValue: '1', + required: true, + variableName: VariableNames.SQLServerScale_VariableName, + }, { + type: FieldType.Number, + label: localize('deployCluster.ComputePoolInstances', "Compute pool instances"), + min: 1, + max: 100, + defaultValue: '1', + useCustomValidator: true, + required: true, + variableName: VariableNames.ComputePoolScale_VariableName, + }] }, { fields: [{ type: FieldType.Number, - label: localize('deployCluster.DataText', "Data"), + label: localize('deployCluster.DataPoolInstances', "Data pool instances"), min: 1, max: 100, defaultValue: '1', useCustomValidator: true, required: true, variableName: VariableNames.DataPoolScale_VariableName, + }, { + type: FieldType.Number, + label: localize('deployCluster.SparkPoolInstances', "Spark pool instances"), + min: 0, + max: 100, + defaultValue: '0', + useCustomValidator: true, + required: true, + variableName: VariableNames.SparkPoolScale_VariableName }] }, { fields: [ { type: FieldType.Number, - label: localize('deployCluster.HDFSText', "HDFS"), + label: localize('deployCluster.StoragePoolInstances', "Storage pool (HDFS) instances"), min: 1, max: 100, defaultValue: '1', @@ -87,90 +110,12 @@ export class ServiceSettingsPage extends WizardPageBase { variableName: VariableNames.HDFSPoolScale_VariableName }, { type: FieldType.Checkbox, - label: localize('deployCluster.includeSparkInHDFSPool', "Include Spark"), + label: localize('deployCluster.IncludeSparkInStoragePool', "Include Spark in storage pool"), defaultValue: 'true', variableName: VariableNames.IncludeSpark_VariableName, required: false } ] - }, { - fields: [ - { - type: FieldType.Number, - label: localize('deployCluster.SparkText', "Spark"), - min: 0, - max: 100, - defaultValue: '0', - useCustomValidator: true, - required: true, - variableName: VariableNames.SparkPoolScale_VariableName - } - ] - } - ] - }; - - const hadrSectionInfo: SectionInfo = { - title: localize('deployCluster.HadrSection', "High availability settings"), - labelWidth: labelWidth, - inputWidth: inputWidth, - spaceBetweenFields: spaceBetweenFields, - rows: [{ - fields: [ - { - type: FieldType.Options, - label: localize('deployCluster.MasterSqlText', "SQL Server Master"), - options: ['1', '3', '4', '5', '6', '7', '8', '9'], - defaultValue: '1', - required: true, - variableName: VariableNames.SQLServerScale_VariableName, - }, { - type: FieldType.Checkbox, - label: localize('deployCluster.EnableHADR', "Enable Availability Groups"), - defaultValue: 'false', - variableName: VariableNames.EnableHADR_VariableName, - required: false - } - ] - }, { - fields: [ - { - type: FieldType.Number, - label: localize('deployCluster.HDFSNameNodeText', "HDFS name node"), - min: 1, - max: 100, - defaultValue: '1', - useCustomValidator: true, - required: true, - variableName: VariableNames.HDFSNameNodeScale_VariableName - } - ] - }, { - fields: [ - { - type: FieldType.Number, - label: localize('deployCluster.SparkHeadText', "SparkHead"), - min: 0, - max: 100, - defaultValue: '1', - useCustomValidator: true, - required: true, - variableName: VariableNames.SparkHeadScale_VariableName - } - ] - }, { - fields: [ - { - type: FieldType.Number, - label: localize('deployCluster.ZooKeeperText', "ZooKeeper"), - min: 0, - max: 100, - defaultValue: '1', - useCustomValidator: true, - required: true, - variableName: VariableNames.ZooKeeperScale_VariableName - } - ] } ] }; @@ -255,7 +200,7 @@ export class ServiceSettingsPage extends WizardPageBase { fields: [ { type: FieldType.Text, - label: localize('deployCluster.HDFSText', "HDFS"), + label: localize('deployCluster.StoragePool', "Storage pool (HDFS)"), required: false, variableName: VariableNames.HDFSDataStorageClassName_VariableName, placeHolder: hintTextForStorageFields, @@ -286,7 +231,7 @@ export class ServiceSettingsPage extends WizardPageBase { fields: [ { type: FieldType.Text, - label: localize('deployCluster.DataText', "Data"), + label: localize('deployCluster.DataPool', "Data pool"), required: false, variableName: VariableNames.DataPoolDataStorageClassName_VariableName, labelWidth: labelWidth, @@ -364,25 +309,22 @@ export class ServiceSettingsPage extends WizardPageBase { }); }; const scaleSection = createSectionFunc(scaleSectionInfo); - const hadrSection = createSectionFunc(hadrSectionInfo); - const endpointSection = this.createEndpointSection(view); + this.endpointSection = this.createEndpointSection(view); const storageSection = createSectionFunc(storageSectionInfo); const advancedStorageSection = createSectionFunc(advancedStorageSectionInfo); const storageContainer = createGroupContainer(view, [storageSection, advancedStorageSection], { header: localize('deployCluster.StorageSectionTitle', "Storage settings"), collapsible: true }); - this.setSQLServerMasterFieldEventHandler(); + + this.handleSparkSettingEvents(); const form = view.modelBuilder.formContainer().withFormItems([ { title: '', component: scaleSection }, { title: '', - component: hadrSection - }, { - title: '', - component: endpointSection + component: this.endpointSection }, { title: '', component: storageContainer @@ -392,86 +334,120 @@ export class ServiceSettingsPage extends WizardPageBase { }); } + private handleSparkSettingEvents(): void { + const sparkInstanceInput = getInputBoxComponent(VariableNames.SparkPoolScale_VariableName, this.inputComponents); + const includeSparkCheckbox = getCheckboxComponent(VariableNames.IncludeSpark_VariableName, this.inputComponents); + this.wizard.registerDisposable(includeSparkCheckbox.onChanged(() => { + if (!includeSparkCheckbox.checked && !(sparkInstanceInput.value && Number.parseInt(sparkInstanceInput.value) > 0)) { + sparkInstanceInput.value = '1'; + } + })); + } + private createEndpointSection(view: azdata.ModelView): azdata.GroupContainer { this.endpointNameColumnHeader = createLabel(view, { text: '', width: labelWidth }); this.dnsColumnHeader = createLabel(view, { text: localize('deployCluster.DNSNameHeader', "DNS name"), width: inputWidth }); - this.portColumnHeader = createLabel(view, { text: localize('deployCluster.PortHeader', "Port"), width: PortInputWidth }); + this.portColumnHeader = createLabel(view, { text: localize('deployCluster.PortHeader', "Port"), width: NumberInputWidth }); this.endpointHeaderRow = createFlexContainer(view, [this.endpointNameColumnHeader, this.dnsColumnHeader, this.portColumnHeader]); this.controllerNameLabel = createLabel(view, { text: localize('deployCluster.ControllerText', "Controller"), width: labelWidth, required: true }); this.controllerDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.ControllerDNSName', "Controller DNS name"), required: false, width: inputWidth }); - this.controllerPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ControllerPortName', "Controller port"), required: true, width: PortInputWidth, min: 1 }); + this.controllerPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ControllerPortName', "Controller port"), required: true, width: NumberInputWidth, min: 1 }); this.controllerEndpointRow = createFlexContainer(view, [this.controllerNameLabel, this.controllerDNSInput, this.controllerPortInput]); this.inputComponents[VariableNames.ControllerDNSName_VariableName] = this.controllerDNSInput; this.inputComponents[VariableNames.ControllerPort_VariableName] = this.controllerPortInput; this.SqlServerNameLabel = createLabel(view, { text: localize('deployCluster.MasterSqlText', "SQL Server Master"), width: labelWidth, required: true }); this.sqlServerDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.MasterSQLServerDNSName', "SQL Server Master DNS name"), required: false, width: inputWidth }); - this.sqlServerPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.MasterSQLServerPortName', "SQL Server Master port"), required: true, width: PortInputWidth, min: 1 }); + this.sqlServerPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.MasterSQLServerPortName', "SQL Server Master port"), required: true, width: NumberInputWidth, min: 1 }); this.sqlServerEndpointRow = createFlexContainer(view, [this.SqlServerNameLabel, this.sqlServerDNSInput, this.sqlServerPortInput]); this.inputComponents[VariableNames.SQLServerDNSName_VariableName] = this.sqlServerDNSInput; this.inputComponents[VariableNames.SQLServerPort_VariableName] = this.sqlServerPortInput; this.gatewayNameLabel = createLabel(view, { text: localize('deployCluster.GatewayText', "Gateway"), width: labelWidth, required: true }); this.gatewayDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.GatewayDNSName', "Gateway DNS name"), required: false, width: inputWidth }); - this.gatewayPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.GatewayPortName', "Gateway port"), required: true, width: PortInputWidth, min: 1 }); + this.gatewayPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.GatewayPortName', "Gateway port"), required: true, width: NumberInputWidth, min: 1 }); this.gatewayEndpointRow = createFlexContainer(view, [this.gatewayNameLabel, this.gatewayDNSInput, this.gatewayPortInput]); this.inputComponents[VariableNames.GatewayDNSName_VariableName] = this.gatewayDNSInput; this.inputComponents[VariableNames.GateWayPort_VariableName] = this.gatewayPortInput; + this.serviceProxyNameLabel = createLabel(view, { text: localize('deployCluster.ServiceProxyText', "Management proxy"), width: labelWidth, required: true }); + this.serviceProxyDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.ServiceProxyDNSName', "Management proxy DNS name"), required: false, width: inputWidth }); + this.serviceProxyPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ServiceProxyPortName', "Management proxy port"), required: true, width: NumberInputWidth, min: 1 }); + this.serviceProxyEndpointRow = createFlexContainer(view, [this.serviceProxyNameLabel, this.serviceProxyDNSInput, this.serviceProxyPortInput]); + this.inputComponents[VariableNames.ServiceProxyDNSName_VariableName] = this.serviceProxyDNSInput; + this.inputComponents[VariableNames.ServiceProxyPort_VariableName] = this.serviceProxyPortInput; + + this.appServiceProxyNameLabel = createLabel(view, { text: localize('deployCluster.AppServiceProxyText', "Application proxy"), width: labelWidth, required: true }); + this.appServiceProxyDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.AppServiceProxyDNSName', "Application proxy DNS name"), required: false, width: inputWidth }); + this.appServiceProxyPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.AppServiceProxyPortName', "Application proxy port"), required: true, width: NumberInputWidth, min: 1 }); + this.appServiceProxyEndpointRow = createFlexContainer(view, [this.appServiceProxyNameLabel, this.appServiceProxyDNSInput, this.appServiceProxyPortInput]); + this.inputComponents[VariableNames.AppServiceProxyDNSName_VariableName] = this.appServiceProxyDNSInput; + this.inputComponents[VariableNames.AppServiceProxyPort_VariableName] = this.appServiceProxyPortInput; + this.readableSecondaryNameLabel = createLabel(view, { text: localize('deployCluster.ReadableSecondaryText', "Readable secondary"), width: labelWidth, required: true }); this.readableSecondaryDNSInput = createTextInput(view, { ariaLabel: localize('deployCluster.ReadableSecondaryDNSName', "Readable secondary DNS name"), required: false, width: inputWidth }); - this.readableSecondaryPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ReadableSecondaryPortName', "Readable secondary port"), required: false, width: PortInputWidth, min: 1 }); + this.readableSecondaryPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ReadableSecondaryPortName', "Readable secondary port"), required: false, width: NumberInputWidth, min: 1 }); this.readableSecondaryEndpointRow = createFlexContainer(view, [this.readableSecondaryNameLabel, this.readableSecondaryDNSInput, this.readableSecondaryPortInput]); this.inputComponents[VariableNames.ReadableSecondaryDNSName_VariableName] = this.readableSecondaryDNSInput; this.inputComponents[VariableNames.ReadableSecondaryPort_VariableName] = this.readableSecondaryPortInput; - return createGroupContainer(view, [this.endpointHeaderRow, this.controllerEndpointRow, this.sqlServerEndpointRow, this.gatewayEndpointRow, this.readableSecondaryEndpointRow], { + return createGroupContainer(view, [this.endpointHeaderRow, this.controllerEndpointRow, this.sqlServerEndpointRow, this.gatewayEndpointRow, this.serviceProxyEndpointRow, this.appServiceProxyEndpointRow, this.readableSecondaryEndpointRow], { header: localize('deployCluster.EndpointSettings', "Endpoint settings"), collapsible: true }); } public onEnter(): void { - this.setDropdownValue(VariableNames.SQLServerScale_VariableName); - this.setCheckboxValue(VariableNames.EnableHADR_VariableName); this.setInputBoxValue(VariableNames.ComputePoolScale_VariableName); this.setInputBoxValue(VariableNames.DataPoolScale_VariableName); this.setInputBoxValue(VariableNames.HDFSPoolScale_VariableName); - this.setInputBoxValue(VariableNames.HDFSNameNodeScale_VariableName); this.setInputBoxValue(VariableNames.SparkPoolScale_VariableName); - this.setInputBoxValue(VariableNames.SparkHeadScale_VariableName); - this.setInputBoxValue(VariableNames.ZooKeeperScale_VariableName); this.setCheckboxValue(VariableNames.IncludeSpark_VariableName); - this.setEnableHadrCheckboxState(this.wizard.model.getIntegerValue(VariableNames.SQLServerScale_VariableName)); this.setInputBoxValue(VariableNames.ControllerPort_VariableName); this.setInputBoxValue(VariableNames.SQLServerPort_VariableName); this.setInputBoxValue(VariableNames.GateWayPort_VariableName); + this.setInputBoxValue(VariableNames.ServiceProxyPort_VariableName); + this.setInputBoxValue(VariableNames.AppServiceProxyPort_VariableName); this.setInputBoxValue(VariableNames.ReadableSecondaryPort_VariableName); - + this.setInputBoxValue(VariableNames.GatewayDNSName_VariableName); + this.setInputBoxValue(VariableNames.AppServiceProxyDNSName_VariableName); + this.setInputBoxValue(VariableNames.SQLServerDNSName_VariableName); + this.setInputBoxValue(VariableNames.ReadableSecondaryDNSName_VariableName); + this.setInputBoxValue(VariableNames.ServiceProxyDNSName_VariableName); + this.setInputBoxValue(VariableNames.ControllerDNSName_VariableName); this.setInputBoxValue(VariableNames.ControllerDataStorageClassName_VariableName); this.setInputBoxValue(VariableNames.ControllerDataStorageSize_VariableName); this.setInputBoxValue(VariableNames.ControllerLogsStorageClassName_VariableName); this.setInputBoxValue(VariableNames.ControllerLogsStorageSize_VariableName); - this.endpointHeaderRow.clearItems(); + this.endpointSection.collapsed = this.wizard.model.authenticationMode !== AuthenticationMode.ActiveDirectory; if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) { this.endpointHeaderRow.addItems([this.endpointNameColumnHeader, this.dnsColumnHeader, this.portColumnHeader]); } this.loadEndpointRow(this.controllerEndpointRow, this.controllerNameLabel, this.controllerDNSInput, this.controllerPortInput); this.loadEndpointRow(this.gatewayEndpointRow, this.gatewayNameLabel, this.gatewayDNSInput, this.gatewayPortInput); this.loadEndpointRow(this.sqlServerEndpointRow, this.SqlServerNameLabel, this.sqlServerDNSInput, this.sqlServerPortInput); - this.updateReadableSecondaryEndpointComponents(this.wizard.model.hadrEnabled); + this.loadEndpointRow(this.appServiceProxyEndpointRow, this.appServiceProxyNameLabel, this.appServiceProxyDNSInput, this.appServiceProxyPortInput); + this.loadEndpointRow(this.serviceProxyEndpointRow, this.serviceProxyNameLabel, this.serviceProxyDNSInput, this.serviceProxyPortInput); + const sqlServerScaleDropdown = getDropdownComponent(VariableNames.SQLServerScale_VariableName, this.inputComponents); + const sqlServerScale = this.wizard.model.getIntegerValue(VariableNames.SQLServerScale_VariableName); + if (sqlServerScale > 1) { + sqlServerScaleDropdown.values = ['3', '4', '5', '6', '7', '8', '9']; + this.loadEndpointRow(this.readableSecondaryEndpointRow, this.readableSecondaryNameLabel, this.readableSecondaryDNSInput, this.readableSecondaryPortInput); + } else { + this.readableSecondaryEndpointRow.clearItems(); + sqlServerScaleDropdown.values = ['1']; + } + sqlServerScaleDropdown.value = sqlServerScale.toString(); + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { this.wizard.wizardObject.message = { text: '' }; if (pcInfo.newPage > pcInfo.lastPage) { - const isValid: boolean = !isInputBoxEmpty(getInputBoxComponent(VariableNames.ComputePoolScale_VariableName, this.inputComponents)) + const allInputFilled: boolean = !isInputBoxEmpty(getInputBoxComponent(VariableNames.ComputePoolScale_VariableName, this.inputComponents)) && !isInputBoxEmpty(getInputBoxComponent(VariableNames.DataPoolScale_VariableName, this.inputComponents)) - && !isInputBoxEmpty(getInputBoxComponent(VariableNames.HDFSNameNodeScale_VariableName, this.inputComponents)) && !isInputBoxEmpty(getInputBoxComponent(VariableNames.HDFSPoolScale_VariableName, this.inputComponents)) && !isInputBoxEmpty(getInputBoxComponent(VariableNames.SparkPoolScale_VariableName, this.inputComponents)) - && !isInputBoxEmpty(getInputBoxComponent(VariableNames.SparkHeadScale_VariableName, this.inputComponents)) - && !isInputBoxEmpty(getInputBoxComponent(VariableNames.ZooKeeperScale_VariableName, this.inputComponents)) && !isInputBoxEmpty(getInputBoxComponent(VariableNames.ControllerDataStorageClassName_VariableName, this.inputComponents)) && !isInputBoxEmpty(getInputBoxComponent(VariableNames.ControllerDataStorageSize_VariableName, this.inputComponents)) && !isInputBoxEmpty(getInputBoxComponent(VariableNames.ControllerLogsStorageClassName_VariableName, this.inputComponents)) @@ -479,21 +455,34 @@ export class ServiceSettingsPage extends WizardPageBase { && !isInputBoxEmpty(getInputBoxComponent(VariableNames.ControllerPort_VariableName, this.inputComponents)) && !isInputBoxEmpty(getInputBoxComponent(VariableNames.SQLServerPort_VariableName, this.inputComponents)) && !isInputBoxEmpty(getInputBoxComponent(VariableNames.GateWayPort_VariableName, this.inputComponents)) - && (!getCheckboxComponent(VariableNames.EnableHADR_VariableName, this.inputComponents).checked - || !isInputBoxEmpty(this.readableSecondaryPortInput)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.AppServiceProxyPort_VariableName, this.inputComponents)) + && !isInputBoxEmpty(getInputBoxComponent(VariableNames.ServiceProxyPort_VariableName, this.inputComponents)) + && (getDropdownComponent(VariableNames.SQLServerScale_VariableName, this.inputComponents).value === '1' + || (!isInputBoxEmpty(this.readableSecondaryPortInput) + && (this.wizard.model.authenticationMode !== AuthenticationMode.ActiveDirectory || !isInputBoxEmpty(this.readableSecondaryDNSInput)))) && (this.wizard.model.authenticationMode !== AuthenticationMode.ActiveDirectory || (!isInputBoxEmpty(this.gatewayDNSInput) && !isInputBoxEmpty(this.controllerDNSInput) && !isInputBoxEmpty(this.sqlServerDNSInput) - && !isInputBoxEmpty(this.readableSecondaryDNSInput) + && !isInputBoxEmpty(this.appServiceProxyDNSInput) + && !isInputBoxEmpty(this.serviceProxyDNSInput) )); - if (!isValid) { + const sparkEnabled = Number.parseInt(getInputBoxComponent(VariableNames.SparkPoolScale_VariableName, this.inputComponents).value!) !== 0 + || getCheckboxComponent(VariableNames.IncludeSpark_VariableName, this.inputComponents).checked!; + + let errorMessage: string | undefined; + if (!allInputFilled) { + errorMessage = MissingRequiredInformationErrorMessage; + } else if (!sparkEnabled) { + errorMessage = localize('deployCluster.SparkMustBeIncluded', "Invalid Spark configuration, you must check the 'Include Spark' checkbox or set the 'Spark pool instances' to at least 1."); + } + if (errorMessage) { this.wizard.wizardObject.message = { - text: MissingRequiredInformationErrorMessage, + text: errorMessage, level: azdata.window.MessageLevel.Error }; } - return isValid; + return allInputFilled && sparkEnabled; } return true; }); @@ -514,43 +503,6 @@ export class ServiceSettingsPage extends WizardPageBase { getCheckboxComponent(variableName, this.inputComponents).checked = this.wizard.model.getBooleanValue(variableName); } - private setDropdownValue(variableName: string): void { - getDropdownComponent(variableName, this.inputComponents).value = this.wizard.model.getStringValue(variableName); - } - - private setSQLServerMasterFieldEventHandler() { - const sqlScaleDropdown = getDropdownComponent(VariableNames.SQLServerScale_VariableName, this.inputComponents); - const enableHadrCheckbox = getCheckboxComponent(VariableNames.EnableHADR_VariableName, this.inputComponents); - this.wizard.registerDisposable(sqlScaleDropdown.onValueChanged(() => { - const selectedValue = typeof sqlScaleDropdown.value === 'string' ? sqlScaleDropdown.value : sqlScaleDropdown.value!.name; - this.setEnableHadrCheckboxState(Number.parseInt(selectedValue)); - })); - this.wizard.registerDisposable(enableHadrCheckbox.onChanged(() => { - this.updateReadableSecondaryEndpointComponents(!!enableHadrCheckbox.checked); - })); - } - - private setEnableHadrCheckboxState(sqlInstances: number) { - // 1. it is ok to enable HADR when there is only 1 replica - // 2. if there are multiple replicas, the hadr.enabled switch must be set to true. - const enableHadrCheckbox = getCheckboxComponent(VariableNames.EnableHADR_VariableName, this.inputComponents); - const hadrEnabled = sqlInstances === 1 ? !!enableHadrCheckbox.checked : true; - if (sqlInstances === 1) { - enableHadrCheckbox.enabled = true; - } else { - enableHadrCheckbox.enabled = false; - } - enableHadrCheckbox.checked = hadrEnabled; - this.updateReadableSecondaryEndpointComponents(hadrEnabled); - } - - private updateReadableSecondaryEndpointComponents(hadrEnabled: boolean) { - this.readableSecondaryEndpointRow.clearItems(); - if (hadrEnabled) { - this.loadEndpointRow(this.readableSecondaryEndpointRow, this.readableSecondaryNameLabel, this.readableSecondaryDNSInput, this.readableSecondaryPortInput); - } - } - private loadEndpointRow(row: azdata.FlexContainer, label: azdata.TextComponent, dnsInput: azdata.InputBoxComponent, portInput: azdata.InputBoxComponent): void { row.clearItems(); const itemLayout: azdata.FlexItemLayout = { CSSStyles: { 'margin-right': '20px' } }; diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/summaryPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/summaryPage.ts index c6b735cfa0..95703de55e 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/summaryPage.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/summaryPage.ts @@ -4,24 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; import * as nls from 'vscode-nls'; -import * as vscode from 'vscode'; import { DeployClusterWizard } from '../deployClusterWizard'; -import { SectionInfo, FieldType, LabelPosition, FontStyle, BdcDeploymentType } from '../../../interfaces'; +import { SectionInfo, FieldType, LabelPosition, BdcDeploymentType, FontWeight } from '../../../interfaces'; import { createSection, createGroupContainer, createFlexContainer, createLabel } from '../../modelViewUtils'; import { WizardPageBase } from '../../wizardPageBase'; import * as VariableNames from '../constants'; -import * as os from 'os'; -import { join } from 'path'; -import * as fs from 'fs'; import { AuthenticationMode } from '../deployClusterWizardModel'; -import { BigDataClusterDeploymentProfile } from '../../../services/bigDataClusterDeploymentProfile'; const localize = nls.loadMessageBundle(); export class SummaryPage extends WizardPageBase { private formItems: azdata.FormComponent[] = []; private form!: azdata.FormBuilder; private view!: azdata.ModelView; - private targetDeploymentProfile!: BigDataClusterDeploymentProfile; constructor(wizard: DeployClusterWizard) { super(localize('deployCluster.summaryPageTitle', "Summary"), '', wizard); @@ -30,30 +24,20 @@ export class SummaryPage extends WizardPageBase { public initialize(): void { this.pageObject.registerContent((view: azdata.ModelView) => { this.view = view; - const deploymentJsonSection = createGroupContainer(view, [ - view.modelBuilder.flexContainer().withItems([ - this.createSaveJsonButton(localize('deployCluster.SaveBdcJson', "Save bdc.json"), 'bdc.json', () => { return this.targetDeploymentProfile.getBdcJson(); }), - this.createSaveJsonButton(localize('deployCluster.SaveControlJson', "Save control.json"), 'control.json', () => { return this.targetDeploymentProfile.getControlJson(); }) - ], { - CSSStyles: { 'margin-right': '10px' } - }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component() - ], { - header: localize('deployCluster.DeploymentJSON', "Deployment JSON files"), - collapsible: true - }); - - this.form = view.modelBuilder.formContainer().withFormItems([ - { - title: '', - component: deploymentJsonSection - } - ]); + this.form = view.modelBuilder.formContainer(); return view.initializeModel(this.form!.withLayout({ width: '100%' }).component()); }); } public onEnter() { - this.targetDeploymentProfile = this.wizard.model.createTargetProfile(); + if (this.wizard.model.deploymentTarget === BdcDeploymentType.NewAKS) { + this.wizard.wizardObject.message = { + level: azdata.window.MessageLevel.Information, + text: localize('resourceDeployment.NewAKSBrowserWindowPrompt', "A browser window for logging to Azure will be opened during the SQL Server Big Data Cluster deployment.") + }; + } + this.wizard.saveConfigButton.hidden = false; + this.wizard.scriptToNotebookButton.hidden = false; this.formItems.forEach(item => { this.form!.removeFormItem(item); }); @@ -71,13 +55,13 @@ export class SummaryPage extends WizardPageBase { type: FieldType.ReadonlyText, label: localize('deployCluster.Kubeconfig', "Kube config"), defaultValue: this.wizard.model.getStringValue(VariableNames.KubeConfigPath_VariableName), - fontStyle: FontStyle.Italic + labelFontWeight: FontWeight.Bold }, { type: FieldType.ReadonlyText, label: localize('deployCluster.ClusterContext', "Cluster context"), defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterContext_VariableName), - fontStyle: FontStyle.Italic + labelFontWeight: FontWeight.Bold }] } ] @@ -96,13 +80,13 @@ export class SummaryPage extends WizardPageBase { type: FieldType.ReadonlyText, label: localize('deployCluster.DeploymentProfile', "Deployment profile"), defaultValue: this.wizard.model.getStringValue(VariableNames.DeploymentProfile_VariableName), - fontStyle: FontStyle.Italic + labelFontWeight: FontWeight.Bold }, { type: FieldType.ReadonlyText, label: localize('deployCluster.ClusterName', "Cluster name"), defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterName_VariableName), - fontStyle: FontStyle.Italic + labelFontWeight: FontWeight.Bold }] }, { fields: [ @@ -110,20 +94,92 @@ export class SummaryPage extends WizardPageBase { type: FieldType.ReadonlyText, label: localize('deployCluster.ControllerUsername', "Controller username"), defaultValue: this.wizard.model.getStringValue(VariableNames.AdminUserName_VariableName), - fontStyle: FontStyle.Italic + labelFontWeight: FontWeight.Bold }, { type: FieldType.ReadonlyText, label: localize('deployCluster.AuthenticationMode', "Authentication mode"), defaultValue: this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory ? localize('deployCluster.AuthenticationMode.ActiveDirectory', "Active Directory") : localize('deployCluster.AuthenticationMode.Basic', "Basic"), - fontStyle: FontStyle.Italic + labelFontWeight: FontWeight.Bold } ] } ] }; + if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) { + clusterSectionInfo.rows!.push({ + fields: [ + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.OuDistinguishedName', "Organizational unit"), + defaultValue: this.wizard.model.getStringValue(VariableNames.OrganizationalUnitDistinguishedName_VariableName), + labelFontWeight: FontWeight.Bold + }, + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.DomainControllerFQDNs', "Domain controller FQDNs"), + defaultValue: this.wizard.model.getStringValue(VariableNames.DomainControllerFQDNs_VariableName), + labelFontWeight: FontWeight.Bold + }] + }); + clusterSectionInfo.rows!.push({ + fields: [ + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.DomainDNSIPAddresses', "Domain DNS IP addresses"), + defaultValue: this.wizard.model.getStringValue(VariableNames.DomainDNSIPAddresses_VariableName), + labelFontWeight: FontWeight.Bold + }, + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.DomainDNSName', "Domain DNS name"), + defaultValue: this.wizard.model.getStringValue(VariableNames.DomainDNSName_VariableName), + labelFontWeight: FontWeight.Bold + }] + }); + clusterSectionInfo.rows!.push({ + fields: [ + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.ClusterAdmins', "Cluster admin group"), + defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterAdmins_VariableName), + labelFontWeight: FontWeight.Bold + }, + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.ClusterUsers', "Cluster users"), + defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterUsers_VariableName), + labelFontWeight: FontWeight.Bold + }] + }); + clusterSectionInfo.rows!.push({ + fields: [ + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.AppOwers', "App owners"), + defaultValue: this.wizard.model.getStringValue(VariableNames.AppOwners_VariableName), + labelFontWeight: FontWeight.Bold + }, + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.AppReaders', "App readers"), + defaultValue: this.wizard.model.getStringValue(VariableNames.AppReaders_VariableName), + labelFontWeight: FontWeight.Bold + }] + }); + clusterSectionInfo.rows!.push({ + fields: [ + { + type: FieldType.ReadonlyText, + label: localize('deployCluster.DomainServiceAccountUserName', "Service account username"), + defaultValue: this.wizard.model.getStringValue(VariableNames.DomainServiceAccountUserName_VariableName), + labelFontWeight: FontWeight.Bold + }] + }); + } + const azureSectionInfo: SectionInfo = { labelPosition: LabelPosition.Left, labelWidth: '150px', @@ -135,26 +191,26 @@ export class SummaryPage extends WizardPageBase { type: FieldType.ReadonlyText, label: localize('deployCluster.SubscriptionId', "Subscription id"), defaultValue: this.wizard.model.getStringValue(VariableNames.SubscriptionId_VariableName) || localize('deployCluster.DefaultSubscription', "Default Azure Subscription"), - fontStyle: FontStyle.Italic + labelFontWeight: FontWeight.Bold }, { type: FieldType.ReadonlyText, label: localize('deployCluster.ResourceGroup', "Resource group"), defaultValue: this.wizard.model.getStringValue(VariableNames.ResourceGroup_VariableName), - fontStyle: FontStyle.Italic + labelFontWeight: FontWeight.Bold } ] }, { fields: [ { type: FieldType.ReadonlyText, - label: localize('deployCluster.Region', "Region"), - defaultValue: this.wizard.model.getStringValue(VariableNames.Region_VariableName), - fontStyle: FontStyle.Italic + label: localize('deployCluster.Location', "Location"), + defaultValue: this.wizard.model.getStringValue(VariableNames.Location_VariableName), + labelFontWeight: FontWeight.Bold }, { type: FieldType.ReadonlyText, label: localize('deployCluster.AksClusterName', "AKS cluster name"), defaultValue: this.wizard.model.getStringValue(VariableNames.AksName_VariableName), - fontStyle: FontStyle.Italic + labelFontWeight: FontWeight.Bold } ] }, { @@ -163,12 +219,12 @@ export class SummaryPage extends WizardPageBase { type: FieldType.ReadonlyText, label: localize('deployCluster.VMSize', "VM size"), defaultValue: this.wizard.model.getStringValue(VariableNames.VMSize_VariableName), - fontStyle: FontStyle.Italic + labelFontWeight: FontWeight.Bold }, { type: FieldType.ReadonlyText, label: localize('deployCluster.VMCount', "VM count"), defaultValue: this.wizard.model.getStringValue(VariableNames.VMCount_VariableName), - fontStyle: FontStyle.Italic + labelFontWeight: FontWeight.Bold } ] } @@ -184,68 +240,34 @@ export class SummaryPage extends WizardPageBase { { fields: [{ type: FieldType.ReadonlyText, - label: localize('deployCluster.ComputeText', "Compute"), - defaultValue: this.wizard.model.getStringValue(VariableNames.ComputePoolScale_VariableName), - fontStyle: FontStyle.Italic + label: localize('deployCluster.MasterSqlServerInstances', "SQL Server master instances"), + defaultValue: this.wizard.model.getStringValue(VariableNames.SQLServerScale_VariableName), + labelFontWeight: FontWeight.Bold }, { type: FieldType.ReadonlyText, - label: localize('deployCluster.DataText', "Data"), + label: localize('deployCluster.ComputePoolInstances', "Compute pool instances"), + defaultValue: this.wizard.model.getStringValue(VariableNames.ComputePoolScale_VariableName), + labelFontWeight: FontWeight.Bold + }] + }, { + fields: [{ + type: FieldType.ReadonlyText, + label: localize('deployCluster.DataPoolInstances', "Data pool instances"), defaultValue: this.wizard.model.getStringValue(VariableNames.DataPoolScale_VariableName), - fontStyle: FontStyle.Italic - } - ] + labelFontWeight: FontWeight.Bold + }, { + type: FieldType.ReadonlyText, + label: localize('deployCluster.SparkPoolInstances', "Spark pool instances"), + defaultValue: this.wizard.model.getStringValue(VariableNames.SparkPoolScale_VariableName), + labelFontWeight: FontWeight.Bold + }] }, { - fields: [ - { - type: FieldType.ReadonlyText, - label: localize('deployCluster.HDFSText', "HDFS"), - defaultValue: `${this.wizard.model.getStringValue(VariableNames.HDFSPoolScale_VariableName)} ${this.wizard.model.getBooleanValue(VariableNames.IncludeSpark_VariableName) ? localize('deployCluster.WithSpark', "(Spark included)") : ''}`, - fontStyle: FontStyle.Italic - }, { - type: FieldType.ReadonlyText, - label: localize('deployCluster.SparkText', "Spark"), - defaultValue: this.wizard.model.getStringValue(VariableNames.SparkPoolScale_VariableName), - fontStyle: FontStyle.Italic - } - ] - } - ] - }; - - const hadrSectionInfo: SectionInfo = { - labelPosition: LabelPosition.Left, - labelWidth: '150px', - inputWidth: '200px', - title: localize('deployCluster.HadrSection', "High availability settings"), - rows: [ - { - fields: [ - { - type: FieldType.ReadonlyText, - label: localize('deployCluster.SqlServerText', "SQL Server Master"), - defaultValue: `${this.wizard.model.getStringValue(VariableNames.SQLServerScale_VariableName)} ${this.wizard.model.hadrEnabled ? localize('deployCluster.WithHADR', "(Availability Groups Enabled)") : ''}`, - fontStyle: FontStyle.Italic - }, { - type: FieldType.ReadonlyText, - label: localize('deployCluster.HDFSNameNodeText', "HDFS name node"), - defaultValue: this.wizard.model.getStringValue(VariableNames.HDFSNameNodeScale_VariableName), - fontStyle: FontStyle.Italic - } - ] - }, { - fields: [ - { - type: FieldType.ReadonlyText, - label: localize('deployCluster.ZooKeeperText', "ZooKeeper"), - defaultValue: this.wizard.model.getStringValue(VariableNames.ZooKeeperScale_VariableName), - fontStyle: FontStyle.Italic - }, { - type: FieldType.ReadonlyText, - label: localize('deployCluster.SparkHeadText', "SparkHead"), - defaultValue: this.wizard.model.getStringValue(VariableNames.SparkHeadScale_VariableName), - fontStyle: FontStyle.Italic - } - ] + fields: [{ + type: FieldType.ReadonlyText, + label: localize('deployCluster.StoragePoolInstances', "Storage pool (HDFS) instances"), + defaultValue: `${this.wizard.model.getStringValue(VariableNames.HDFSPoolScale_VariableName)} ${this.wizard.model.getBooleanValue(VariableNames.IncludeSpark_VariableName) ? localize('deployCluster.WithSpark', "(Spark included)") : ''}`, + labelFontWeight: FontWeight.Bold + }] } ] }; @@ -271,7 +293,6 @@ export class SummaryPage extends WizardPageBase { const clusterSection = createSectionFunc(clusterSectionInfo); const scaleSection = createSectionFunc(scaleSectionInfo); - const hadrSection = createSectionFunc(hadrSectionInfo); const endpointSection = { title: '', component: this.createEndpointSection() @@ -285,10 +306,16 @@ export class SummaryPage extends WizardPageBase { this.formItems.push(azureSection); } - this.formItems.push(clusterSection, scaleSection, hadrSection, endpointSection, storageSection); + this.formItems.push(clusterSection, scaleSection, endpointSection, storageSection); this.form.addFormItems(this.formItems); } + public onLeave() { + this.wizard.saveConfigButton.hidden = true; + this.wizard.scriptToNotebookButton.hidden = true; + this.wizard.wizardObject.message = { text: '' }; + } + private getStorageSettingValue(propertyName: string, defaultValuePropertyName: string): string | undefined { const value = this.wizard.model.getStringValue(propertyName); return (value === undefined || value === '') ? this.wizard.model.getStringValue(defaultValuePropertyName) : value; @@ -324,7 +351,7 @@ export class SummaryPage extends WizardPageBase { this.wizard.model.getStringValue(VariableNames.ControllerLogsStorageClassName_VariableName), this.wizard.model.getStringValue(VariableNames.ControllerLogsStorageSize_VariableName)], [ - localize('deployCluster.HDFSText', "HDFS"), + localize('deployCluster.StoragePool', "Storage pool (HDFS)"), this.getStorageSettingValue(VariableNames.HDFSDataStorageClassName_VariableName, VariableNames.ControllerDataStorageClassName_VariableName), this.getStorageSettingValue(VariableNames.HDFSDataStorageSize_VariableName, VariableNames.ControllerDataStorageSize_VariableName), this.getStorageSettingValue(VariableNames.HDFSLogsStorageClassName_VariableName, VariableNames.ControllerLogsStorageClassName_VariableName), @@ -357,10 +384,12 @@ export class SummaryPage extends WizardPageBase { const endpointRows = [ this.createEndpointRow(localize('deployCluster.ControllerText', "Controller"), VariableNames.ControllerDNSName_VariableName, VariableNames.ControllerPort_VariableName), this.createEndpointRow(localize('deployCluster.SqlServerText', "SQL Server Master"), VariableNames.SQLServerDNSName_VariableName, VariableNames.SQLServerPort_VariableName), - this.createEndpointRow(localize('deployCluster.GatewayText', "Gateway"), VariableNames.GatewayDNSName_VariableName, VariableNames.GateWayPort_VariableName) + this.createEndpointRow(localize('deployCluster.GatewayText', "Gateway"), VariableNames.GatewayDNSName_VariableName, VariableNames.GateWayPort_VariableName), + this.createEndpointRow(localize('deployCluster.AppServiceProxyText', "Application proxy"), VariableNames.AppServiceProxyDNSName_VariableName, VariableNames.AppServiceProxyPort_VariableName), + this.createEndpointRow(localize('deployCluster.ServiceProxyText', "Management proxy"), VariableNames.ServiceProxyDNSName_VariableName, VariableNames.ServiceProxyPort_VariableName) ]; - if (this.wizard.model.hadrEnabled) { + if (this.wizard.model.getIntegerValue(VariableNames.SQLServerScale_VariableName) > 1) { endpointRows.push( this.createEndpointRow(localize('deployCluster.ReadableSecondaryText', "Readable secondary"), VariableNames.ReadableSecondaryDNSName_VariableName, VariableNames.ReadableSecondaryPort_VariableName) ); @@ -373,43 +402,15 @@ export class SummaryPage extends WizardPageBase { private createEndpointRow(name: string, dnsVariableName: string, portVariableName: string): azdata.FlexContainer { const items = []; - items.push(createLabel(this.view, { text: name, width: '150px' })); + items.push(createLabel(this.view, { text: name, width: '150px', fontWeight: FontWeight.Bold })); if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) { - items.push(createLabel(this.view, { text: this.wizard.model.getStringValue(dnsVariableName)!, width: '200px', fontStyle: FontStyle.Italic })); + items.push(createLabel(this.view, { + text: this.wizard.model.getStringValue(dnsVariableName)!, width: '200px' + })); } - items.push(createLabel(this.view, { text: this.wizard.model.getStringValue(portVariableName)!, width: '100px', fontStyle: FontStyle.Italic })); + items.push(createLabel(this.view, { + text: this.wizard.model.getStringValue(portVariableName)!, width: '100px' + })); return createFlexContainer(this.view, items); } - - private createSaveJsonButton(label: string, fileName: string, getContent: () => string): azdata.ButtonComponent { - const button = this.view.modelBuilder.button().withProperties({ - title: label, - label: fileName, - ariaLabel: label, - width: '150px' - }).component(); - this.wizard.registerDisposable(button.onDidClick(() => { - vscode.window.showSaveDialog({ - defaultUri: vscode.Uri.file(join(os.homedir(), fileName)), - filters: { - 'JSON': ['json'] - } - }).then((path) => { - if (path) { - fs.promises.writeFile(path.fsPath, getContent()).then(() => { - this.wizard.wizardObject.message = { - text: localize('deployCluster.SaveJsonFileMessage', "File saved: {0}", path.fsPath), - level: azdata.window.MessageLevel.Information - }; - }).catch((error) => { - this.wizard.wizardObject.message = { - text: error.message, - level: azdata.window.MessageLevel.Error - }; - }); - } - }); - })); - return button; - } } diff --git a/extensions/resource-deployment/src/ui/modelViewUtils.ts b/extensions/resource-deployment/src/ui/modelViewUtils.ts index 3d8e889483..8aaaa0f451 100644 --- a/extensions/resource-deployment/src/ui/modelViewUtils.ts +++ b/extensions/resource-deployment/src/ui/modelViewUtils.ts @@ -6,8 +6,9 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import { DialogInfoBase, FieldType, FieldInfo, SectionInfo, LabelPosition } from '../interfaces'; +import { DialogInfoBase, FieldType, FieldInfo, SectionInfo, LabelPosition, FontWeight, FontStyle } from '../interfaces'; import { Model } from './model'; +import { getDateTimeString } from '../utils'; const localize = nls.loadMessageBundle(); @@ -40,7 +41,6 @@ export interface WizardPageContext extends CreateContext { container: azdata.window.Wizard; } - export interface SectionContext extends CreateContext { sectionInfo: SectionInfo; view: azdata.ModelView; @@ -70,12 +70,13 @@ export function createTextInput(view: azdata.ModelView, inputInfo: { defaultValu }).component(); } -export function createLabel(view: azdata.ModelView, info: { text: string, description?: string, required?: boolean, width?: string, fontStyle?: string }): azdata.TextComponent { +export function createLabel(view: azdata.ModelView, info: { text: string, description?: string, required?: boolean, width?: string, fontStyle?: FontStyle, fontWeight?: FontWeight, links?: azdata.LinkArea[] }): azdata.TextComponent { const text = view.modelBuilder.text().withProperties({ value: info.text, description: info.description, requiredIndicator: info.required, - CSSStyles: { 'font-style': info.fontStyle || 'normal' } + CSSStyles: { 'font-style': info.fontStyle || 'normal', 'font-weight': info.fontWeight || 'normal' }, + links: info.links }).component(); text.width = info.width; return text; @@ -101,11 +102,13 @@ export function createCheckbox(view: azdata.ModelView, info: { initialValue: boo }).component(); } -export function createDropdown(view: azdata.ModelView, info: { defaultValue?: string | azdata.CategoryValue, values?: string[] | azdata.CategoryValue[], width?: string }): azdata.DropDownComponent { +export function createDropdown(view: azdata.ModelView, info: { defaultValue?: string | azdata.CategoryValue, values?: string[] | azdata.CategoryValue[], width?: string, editable?: boolean, required?: boolean }): azdata.DropDownComponent { return view.modelBuilder.dropDown().withProperties({ values: info.values, value: info.defaultValue, - width: info.width + width: info.width, + editable: info.editable, + fireOnTextChange: true }).component(); } @@ -232,7 +235,6 @@ function addLabelInputPairToContainer(view: azdata.ModelView, components: azdata } } - function processField(context: FieldContext): void { switch (context.fieldInfo.type) { case FieldType.Options: @@ -263,19 +265,21 @@ function processField(context: FieldContext): void { } function processOptionsTypeField(context: FieldContext): void { - const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: false, width: context.fieldInfo.labelWidth }); + const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight }); const dropdown = createDropdown(context.view, { values: context.fieldInfo.options, defaultValue: context.fieldInfo.defaultValue, - width: context.fieldInfo.inputWidth + width: context.fieldInfo.inputWidth, + editable: context.fieldInfo.editable, + required: context.fieldInfo.required }); context.onNewInputComponentCreated(context.fieldInfo.variableName!, dropdown); addLabelInputPairToContainer(context.view, context.components, label, dropdown, context.fieldInfo.labelPosition); } function processDateTimeTextField(context: FieldContext): void { - const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth }); - const defaultValue = context.fieldInfo.defaultValue + new Date().toISOString().slice(0, 19).replace(/[^0-9]/g, ''); // Take the date time information and only leaving the numbers + const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight }); + const defaultValue = context.fieldInfo.defaultValue + getDateTimeString(); const input = context.view.modelBuilder.inputBox().withProperties({ value: defaultValue, ariaLabel: context.fieldInfo.label, @@ -289,7 +293,7 @@ function processDateTimeTextField(context: FieldContext): void { } function processNumberField(context: FieldContext): void { - const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth }); + const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight }); const input = createNumberInput(context.view, { defaultValue: context.fieldInfo.defaultValue, ariaLabel: context.fieldInfo.label, @@ -304,7 +308,7 @@ function processNumberField(context: FieldContext): void { } function processTextField(context: FieldContext): void { - const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth }); + const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight }); const input = createTextInput(context.view, { defaultValue: context.fieldInfo.defaultValue, ariaLabel: context.fieldInfo.label, @@ -317,7 +321,7 @@ function processTextField(context: FieldContext): void { } function processPasswordField(context: FieldContext): void { - const passwordLabel = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth }); + const passwordLabel = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight }); const passwordInput = context.view.modelBuilder.inputBox().withProperties({ ariaLabel: context.fieldInfo.label, inputType: 'password', @@ -343,7 +347,7 @@ function processPasswordField(context: FieldContext): void { if (context.fieldInfo.confirmationRequired) { const passwordNotMatchMessage = getPasswordMismatchMessage(context.fieldInfo.label); - const confirmPasswordLabel = createLabel(context.view, { text: context.fieldInfo.confirmationLabel!, required: true, width: context.fieldInfo.labelWidth }); + const confirmPasswordLabel = createLabel(context.view, { text: context.fieldInfo.confirmationLabel!, required: true, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight }); const confirmPasswordInput = context.view.modelBuilder.inputBox().withProperties({ ariaLabel: context.fieldInfo.confirmationLabel, inputType: 'password', @@ -373,8 +377,8 @@ function processPasswordField(context: FieldContext): void { } function processReadonlyTextField(context: FieldContext): void { - const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: false, width: context.fieldInfo.labelWidth }); - const text = createLabel(context.view, { text: context.fieldInfo.defaultValue!, description: '', required: false, width: context.fieldInfo.inputWidth, fontStyle: context.fieldInfo.fontStyle }); + const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: false, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight }); + const text = createLabel(context.view, { text: context.fieldInfo.defaultValue!, description: '', required: false, width: context.fieldInfo.inputWidth, fontStyle: context.fieldInfo.fontStyle, links: context.fieldInfo.links }); addLabelInputPairToContainer(context.view, context.components, label, text, context.fieldInfo.labelPosition); } diff --git a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts index 7916036207..aa9c81f5a3 100644 --- a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts +++ b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts @@ -52,7 +52,9 @@ export class ResourceTypePickerDialog extends DialogBase { tab.registerContent((view: azdata.ModelView) => { const tableWidth = 1126; this._view = view; - this.resourceTypeService.getResourceTypes().forEach(resourceType => this.addCard(resourceType)); + this.resourceTypeService.getResourceTypes().sort((a: ResourceType, b: ResourceType) => { + return (a.displayIndex || Number.MAX_VALUE) - (b.displayIndex || Number.MAX_VALUE); + }).forEach(resourceType => this.addCard(resourceType)); const cardsContainer = view.modelBuilder.flexContainer().withItems(this._resourceTypeCards, { flex: '0 0 auto', CSSStyles: { 'margin-bottom': '10px' } }).withLayout({ flexFlow: 'row' }).component(); this._resourceDescriptionLabel = view.modelBuilder.text().withProperties({ value: this._selectedResourceType ? this._selectedResourceType.description : undefined }).component(); this._optionsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); diff --git a/extensions/resource-deployment/src/utils.ts b/extensions/resource-deployment/src/utils.ts new file mode 100644 index 0000000000..fcb2797aae --- /dev/null +++ b/extensions/resource-deployment/src/utils.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function getErrorMessage(error: string | Error): string { + return typeof error === 'string' ? error : error.message; +} + +export function getDateTimeString(): string { + return new Date().toISOString().slice(0, 19).replace(/[^0-9]/g, ''); // Take the date time information and only leaving the numbers +} diff --git a/src/sql/azdata.d.ts b/src/sql/azdata.d.ts index a4c5a90381..a672e7203c 100644 --- a/src/sql/azdata.d.ts +++ b/src/sql/azdata.d.ts @@ -2895,7 +2895,7 @@ declare module 'azdata' { export interface FormContainer extends Container { } - export interface GroupContainer extends Container { + export interface GroupContainer extends Container, GroupContainerProperties { } @@ -3114,6 +3114,10 @@ declare module 'azdata' { } + export interface GroupContainerProperties { + collapsed: boolean; + } + export interface LinkArea { text: string; url: string; @@ -3527,7 +3531,7 @@ declare module 'azdata' { * Create a button which can be included in a dialog * @param label The label of the button */ - export function createButton(label: string): Button; + export function createButton(label: string, position?: DialogButtonPosition): Button; /** * Opens the given dialog if it is not already open @@ -3691,8 +3695,15 @@ declare module 'azdata' { * Raised when the button is clicked */ readonly onClick: vscode.Event; + + /** + * Position of the button on the dialog footer + */ + position?: DialogButtonPosition; } + export type DialogButtonPosition = 'left' | 'right'; + export interface WizardPageChangeInfo { /** * The page number that the wizard changed from diff --git a/src/sql/platform/dialog/browser/dialogModal.ts b/src/sql/platform/dialog/browser/dialogModal.ts index 4966729f41..9e6d14d95d 100644 --- a/src/sql/platform/dialog/browser/dialogModal.ts +++ b/src/sql/platform/dialog/browser/dialogModal.ts @@ -91,7 +91,7 @@ export class DialogModal extends Modal { } private addDialogButton(button: DialogButton, onSelect: () => void = () => undefined, registerClickEvent: boolean = true, requireDialogValid: boolean = false): Button { - let buttonElement = this.addFooterButton(button.label, onSelect); + let buttonElement = this.addFooterButton(button.label, onSelect, button.position); buttonElement.enabled = button.enabled; if (registerClickEvent) { button.registerClickEvent(buttonElement.onDidClick); diff --git a/src/sql/platform/dialog/browser/wizardModal.ts b/src/sql/platform/dialog/browser/wizardModal.ts index 8658fdb7bb..c29f4b55f4 100644 --- a/src/sql/platform/dialog/browser/wizardModal.ts +++ b/src/sql/platform/dialog/browser/wizardModal.ts @@ -102,7 +102,7 @@ export class WizardModal extends Modal { } private addDialogButton(button: DialogButton, onSelect: () => void = () => undefined, registerClickEvent: boolean = true, requirePageValid: boolean = false): Button { - let buttonElement = this.addFooterButton(button.label, onSelect); + let buttonElement = this.addFooterButton(button.label, onSelect, button.position); buttonElement.enabled = button.enabled; if (registerClickEvent) { button.registerClickEvent(buttonElement.onDidClick); diff --git a/src/sql/platform/dialog/common/dialogTypes.ts b/src/sql/platform/dialog/common/dialogTypes.ts index f11709c86a..30e8e23230 100644 --- a/src/sql/platform/dialog/common/dialogTypes.ts +++ b/src/sql/platform/dialog/common/dialogTypes.ts @@ -84,6 +84,7 @@ export class DialogButton implements azdata.window.Button { private _enabled: boolean; private _hidden: boolean; private _focused: boolean; + private _position?: azdata.window.DialogButtonPosition; private _onClick: Emitter = new Emitter(); public readonly onClick: Event = this._onClick.event; private _onUpdate: Emitter = new Emitter(); @@ -131,6 +132,15 @@ export class DialogButton implements azdata.window.Button { this._onUpdate.fire(); } + public get position(): azdata.window.DialogButtonPosition | undefined { + return this._position; + } + + public set position(value: azdata.window.DialogButtonPosition | undefined) { + this._position = value; + this._onUpdate.fire(); + } + /** * Register an event that notifies the button that it has been clicked */ diff --git a/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts b/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts index 68cdfef763..1a03a811e8 100644 --- a/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts +++ b/src/sql/workbench/api/browser/mainThreadModelViewDialog.ts @@ -126,6 +126,7 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape let button = this._buttons.get(handle); if (!button) { button = new DialogButton(details.label, details.enabled); + button.position = details.position; button.hidden = details.hidden; button.onClick(() => this.onButtonClick(handle)); this._buttons.set(handle, button); @@ -134,6 +135,7 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape button.enabled = details.enabled; button.hidden = details.hidden; button.focused = details.focused; + button.position = details.position; } return Promise.resolve(); diff --git a/src/sql/workbench/api/common/extHostModelView.ts b/src/sql/workbench/api/common/extHostModelView.ts index 1730d730f0..efc45c19a6 100644 --- a/src/sql/workbench/api/common/extHostModelView.ts +++ b/src/sql/workbench/api/common/extHostModelView.ts @@ -74,7 +74,7 @@ class ModelBuilderImpl implements azdata.ModelBuilder { groupContainer(): azdata.GroupBuilder { let id = this.getNextComponentId(); - let container: GenericContainerBuilder = new GenericContainerBuilder(this._proxy, this._handle, ModelComponentTypes.Group, id); + let container = new GroupContainerBuilder(this._proxy, this._handle, ModelComponentTypes.Group, id); this._componentBuilders.set(id, container); return container; } @@ -422,6 +422,12 @@ class FormContainerBuilder extends GenericContainerBuilder { + constructor(proxy: MainThreadModelViewShape, handle: number, type: ModelComponentTypes, id: string) { + super(new GroupContainerComponentWrapper(proxy, handle, type, id)); + } +} + class ToolbarContainerBuilder extends GenericContainerBuilder implements azdata.ToolbarBuilder { withToolbarItems(components: azdata.ToolbarComponent[]): azdata.ContainerBuilder { this._component.itemConfigs = components.map(item => { @@ -1291,7 +1297,7 @@ class DropDownWrapper extends ComponentWrapper implements azdata.DropDownCompone public get value(): string | azdata.CategoryValue { let val = this.properties['value']; - if (!val && this.values && this.values.length > 0) { + if (!this.editable && !val && this.values && this.values.length > 0) { val = this.values[0]; } return val; @@ -1545,6 +1551,19 @@ class HyperlinkComponentWrapper extends ComponentWrapper implements azdata.Hyper } } +class GroupContainerComponentWrapper extends ComponentWrapper implements azdata.GroupContainer { + constructor(proxy: MainThreadModelViewShape, handle: number, type: ModelComponentTypes, id: string) { + super(proxy, handle, type, id); + this.properties = {}; + } + public get collapsed(): boolean { + return this.properties['collapsed']; + } + public set collapsed(v: boolean) { + this.setProperty('collapsed', v); + } +} + class ModelViewImpl implements azdata.ModelView { public onClosedEmitter = new Emitter(); diff --git a/src/sql/workbench/api/common/extHostModelViewDialog.ts b/src/sql/workbench/api/common/extHostModelViewDialog.ts index 27240ef8f0..9961fd0527 100644 --- a/src/sql/workbench/api/common/extHostModelViewDialog.ts +++ b/src/sql/workbench/api/common/extHostModelViewDialog.ts @@ -208,6 +208,7 @@ class ButtonImpl implements azdata.window.Button { private _enabled: boolean; private _hidden: boolean; private _focused: boolean; + private _position: azdata.window.DialogButtonPosition; private _onClick = new Emitter(); public onClick = this._onClick.event; @@ -215,6 +216,7 @@ class ButtonImpl implements azdata.window.Button { constructor(private _extHostModelViewDialog: ExtHostModelViewDialog) { this._enabled = true; this._hidden = false; + this._position = 'right'; } public get label(): string { @@ -244,6 +246,15 @@ class ButtonImpl implements azdata.window.Button { this._extHostModelViewDialog.updateButton(this); } + public get position(): azdata.window.DialogButtonPosition { + return this._position; + } + + public set position(value: azdata.window.DialogButtonPosition) { + this._position = value; + this._extHostModelViewDialog.updateButton(this); + } + public get focused(): boolean { return this._focused; } @@ -471,10 +482,10 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { return handle; } - private getHandle(item: azdata.window.Button | azdata.window.Dialog | azdata.window.DialogTab - | azdata.window.ModelViewPanel | azdata.window.Wizard | azdata.window.WizardPage | azdata.workspace.ModelViewEditor) { + public getHandle(item: azdata.window.Button | azdata.window.Dialog | azdata.window.DialogTab + | azdata.window.ModelViewPanel | azdata.window.Wizard | azdata.window.WizardPage | azdata.workspace.ModelViewEditor, createIfNotFound: boolean = true) { let handle = this._objectHandles.get(item); - if (handle === undefined) { + if (createIfNotFound && handle === undefined) { handle = ExtHostModelViewDialog.getNewHandle(); this._objectHandles.set(item, handle); this._objectsByHandle.set(handle, item); @@ -587,7 +598,8 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { label: button.label, enabled: button.enabled, hidden: button.hidden, - focused: button.focused + focused: button.focused, + position: button.position }); } @@ -614,11 +626,12 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { return tab; } - public createButton(label: string): azdata.window.Button { + public createButton(label: string, position: azdata.window.DialogButtonPosition = 'right'): azdata.window.Button { let button = new ButtonImpl(this); this.getHandle(button); this.registerOnClickCallback(button, button.getOnClickCallback()); button.label = label; + button.position = position; return button; } diff --git a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts index e246ce4827..8d1caafc74 100644 --- a/src/sql/workbench/api/common/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/common/sqlExtHost.api.impl.ts @@ -413,8 +413,8 @@ export function createAdsApiFactory(accessor: ServicesAccessor): IAdsExtensionAp createTab(title: string): azdata.window.DialogTab { return extHostModelViewDialog.createTab(title, extension); }, - createButton(label: string): azdata.window.Button { - return extHostModelViewDialog.createButton(label); + createButton(label: string, position: azdata.window.DialogButtonPosition = 'right'): azdata.window.Button { + return extHostModelViewDialog.createButton(label, position); }, openDialog(dialog: azdata.window.Dialog) { return extHostModelViewDialog.openDialog(dialog); diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 573aca390e..43cb19f8f1 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -253,6 +253,7 @@ export interface IModelViewButtonDetails { enabled: boolean; hidden: boolean; focused?: boolean; + position?: 'left' | 'right'; } export interface IModelViewWizardPageDetails { diff --git a/src/sql/workbench/browser/modelComponents/groupContainer.component.ts b/src/sql/workbench/browser/modelComponents/groupContainer.component.ts index f4ca77ac4e..739feab2ae 100644 --- a/src/sql/workbench/browser/modelComponents/groupContainer.component.ts +++ b/src/sql/workbench/browser/modelComponents/groupContainer.component.ts @@ -10,7 +10,7 @@ import { } from '@angular/core'; import { IComponent, IComponentDescriptor, IModelStore } from 'sql/workbench/browser/modelComponents/interfaces'; -import { GroupLayout } from 'azdata'; +import { GroupLayout, GroupContainerProperties } from 'azdata'; import { ContainerBase } from 'sql/workbench/browser/modelComponents/componentBase'; @@ -37,7 +37,6 @@ export default class GroupContainer extends ContainerBase implement @Input() modelStore: IModelStore; private _containerLayout: GroupLayout; - private _collapsed: boolean; @ViewChild('container', { read: ElementRef }) private _container: ElementRef; @@ -45,7 +44,7 @@ export default class GroupContainer extends ContainerBase implement @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef, @Inject(forwardRef(() => ElementRef)) el: ElementRef) { super(changeRef, el); - this._collapsed = false; + this.collapsed = false; } ngOnInit(): void { @@ -63,10 +62,18 @@ export default class GroupContainer extends ContainerBase implement public setLayout(layout: GroupLayout): void { this._containerLayout = layout; - this._collapsed = !!layout.collapsed; + this.collapsed = !!layout.collapsed; this.layout(); } + public set collapsed(newValue: boolean) { + this.setPropertyFromUI((properties, value) => { properties.collapsed = value; }, newValue); + } + + public get collapsed(): boolean { + return this.getPropertyOrDefault((props) => props.collapsed, false); + } + private hasHeader(): boolean { return this._containerLayout && this._containerLayout && this._containerLayout.header !== undefined; } @@ -88,12 +95,12 @@ export default class GroupContainer extends ContainerBase implement } private getContainerDisplayStyle(): string { - return !this.isCollapsible() || !this._collapsed ? 'block' : 'none'; + return !this.isCollapsible() || !this.collapsed ? 'block' : 'none'; } private getHeaderClass(): string { if (this.isCollapsible()) { - let modifier = this._collapsed ? 'collapsed' : 'expanded'; + let modifier = this.collapsed ? 'collapsed' : 'expanded'; return `modelview-group-header-collapsible ${modifier}`; } else { return 'modelview-group-header'; @@ -102,7 +109,7 @@ export default class GroupContainer extends ContainerBase implement private changeState(): void { if (this.isCollapsible()) { - this._collapsed = !this._collapsed; + this.collapsed = !this.collapsed; this._changeRef.detectChanges(); } } diff --git a/src/sql/workbench/contrib/welcome/page/browser/az_data_welcome_page.ts b/src/sql/workbench/contrib/welcome/page/browser/az_data_welcome_page.ts index f0dad0102b..64966d2f9f 100644 --- a/src/sql/workbench/contrib/welcome/page/browser/az_data_welcome_page.ts +++ b/src/sql/workbench/contrib/welcome/page/browser/az_data_welcome_page.ts @@ -41,9 +41,7 @@ export default () => `
diff --git a/src/sql/workbench/test/electron-browser/api/extHostModelViewDialog.test.ts b/src/sql/workbench/test/electron-browser/api/extHostModelViewDialog.test.ts index f1b4a9d360..06250cb687 100644 --- a/src/sql/workbench/test/electron-browser/api/extHostModelViewDialog.test.ts +++ b/src/sql/workbench/test/electron-browser/api/extHostModelViewDialog.test.ts @@ -105,10 +105,6 @@ suite('ExtHostModelViewDialog Tests', () => { }); test('Button clicks are forwarded to the correct button', () => { - // Set up the proxy to record button handles - let handles = []; - mockProxy.setup(x => x.$setButtonDetails(It.isAny(), It.isAny())).callback((handle, details) => handles.push(handle)); - // Set up the buttons to record click events let label1 = 'button_1'; let label2 = 'button_2'; @@ -117,14 +113,16 @@ suite('ExtHostModelViewDialog Tests', () => { let clickEvents = []; button1.onClick(() => clickEvents.push(1)); button2.onClick(() => clickEvents.push(2)); + const button1Handle = extHostModelViewDialog.getHandle(button1, false); + const button2Handle = extHostModelViewDialog.getHandle(button2, false); extHostModelViewDialog.updateButton(button1); extHostModelViewDialog.updateButton(button2); // If the main thread sends some notifications that the buttons have been clicked - extHostModelViewDialog.$onButtonClick(handles[0]); - extHostModelViewDialog.$onButtonClick(handles[1]); - extHostModelViewDialog.$onButtonClick(handles[1]); - extHostModelViewDialog.$onButtonClick(handles[0]); + extHostModelViewDialog.$onButtonClick(button1Handle); + extHostModelViewDialog.$onButtonClick(button2Handle); + extHostModelViewDialog.$onButtonClick(button2Handle); + extHostModelViewDialog.$onButtonClick(button1Handle); // Then the clicks should have been handled by the expected handlers assert.deepEqual(clickEvents, [1, 2, 2, 1]);