Compare commits

...

27 Commits

Author SHA1 Message Date
Karl Burtram
13e3627627 Update the path to the smoke test (#15139) 2021-04-14 16:51:10 -07:00
Aditya Bist
e2111fe493 Links handling in commands (#15118) (#15137)
* format

* update external options type

* add command flag for command

* Allow commands in Notebooks

Co-authored-by: chgagnon <chgagnon@microsoft.com>
(cherry picked from commit e151668c81)
2021-04-14 15:35:15 -07:00
Aditya Bist
7b46269b44 add flag for proxy (#15120) (#15126)
* add flag for proxy

* update distro hash

* Bump distro hash

* Bump distro

Co-authored-by: kburtram <karlb@microsoft.com>
Co-authored-by: chgagnon <chgagnon@microsoft.com>
(cherry picked from commit b6bdb68596)
2021-04-14 10:22:17 -07:00
Aditya Bist
69b9a19634 fixed css for font family (#15119) (#15124)
(cherry picked from commit 90868bc0ad)
2021-04-13 21:15:48 -07:00
Aditya Bist
ebe835ec99 Remote CLI fixes (#15117) (#15125)
* fixes from vscode

* update distro

* fix distro

* fix hygiene

* reorder hygiene

* Update distro

Co-authored-by: chgagnon <chgagnon@microsoft.com>
(cherry picked from commit 1b78008258)
2021-04-13 21:15:22 -07:00
Justin M
d4e25f4d89 Bumped version of Kusto to 0.5.3 for SqlToolService 92 (#15116) 2021-04-13 13:54:50 -07:00
Charles Gagnon
809d4de862 Only reveal parent node when finding notebook item (#15091) (#15103)
(cherry picked from commit 42cd650147)
2021-04-12 16:46:30 -07:00
Chris LaFreniere
e1aae951a3 Change to fspath from resource tostring (#15069) (#15078) 2021-04-09 14:21:29 -07:00
Sakshi Sharma
bdd99bd0d8 Fix deploy data to be presented on dashboard (#15073) (#15080)
* Fix deploy data to be presented on dashboard

* Bump sql-db-project extensions version

* Address comment
2021-04-09 13:26:56 -07:00
Barbara Valdez
c06bfad916 fix search loading animation (#15063) (#15066) 2021-04-08 16:45:06 -07:00
Maddy
1c91f7971e Select active notebook in viewlet when opened from command pallet (#15027) (#15065)
* await initialized

* fixes specific for windows

* address comments

* update comment
2021-04-08 16:44:08 -07:00
Barbara Valdez
8c9037fbdf fix anchor links in wysiwyg (#14950) (#15054)
* fix anchor links in wysiwyg
2021-04-08 12:51:20 -07:00
Udeesha Gautam
d029bb9602 Fixing an issue where backup launch was freezing ADS and restore launch was not doiing anything. This repros in absence of any connetcions. (#15041) (#15044) 2021-04-08 09:06:58 -07:00
Udeesha Gautam
f0558714a4 Fix for Bug #15020 dashboard update with multiproject (#15029) (#15042)
* Fix to enable two projects showing only their data even if opening together

* Fixing a typo

* Taking in PR comments
2021-04-07 18:37:42 -07:00
Charles Gagnon
49147305e8 Fix event property name (#15026) (#15031)
(cherry picked from commit 1c66499910)
2021-04-07 16:15:34 -07:00
Barbara Valdez
519012c690 bump sqlservernotebook version (#15025) (#15039) 2021-04-07 15:51:20 -07:00
Udeesha Gautam
ff05bc2b03 Add message when no history exists on projects dashboard (#15002) (#15019)
* Add message when no history exists on projects dashboard

* Bump version for sql db projects

* Update text, add refresh button

* Remove commented code

Co-authored-by: Sakshi Sharma <57200045+SakshiS-harma@users.noreply.github.com>
2021-04-07 13:22:00 -07:00
Charles Gagnon
b54beb6e7a Add required minimum version for azdata extension (#15010) (#15015)
* Add check for minimum required azdata version

* cleanup

* remove unused

* comment

* param comment

* Fix tests
2021-04-07 10:53:08 -07:00
Kim Santiago
dbdcc3d20a fix schema compare dropdown not selecting correct db when multiple active connections (#14999) (#15012)
* fix schema compare dropdown not selecting correct db when multiple active connections

* fix when no username so default is used
2021-04-06 19:34:55 -07:00
Justin M
cf48776710 Increased required version for Kusto from 1.27 to 1.28 (#15001) 2021-04-06 14:05:37 -07:00
Vasu Bhog
309f750b92 See and Edit Selected Links in Callout Dialog (#14987) (#15000)
* Add URL label to linkCallout

* add test for file link
2021-04-06 13:26:51 -07:00
Charles Gagnon
8c92af3016 [Port] Update minimum required azdata version (#14998)
* Update minimum required azdata version to latest release (#14995)

* Update minimum required azdata engine version - Arc (#14996)

* vBump Arc/Azdata

* undo vbump

* vBump
2021-04-06 11:25:26 -07:00
Charles Gagnon
9b117da9cb Sign vulkan-1 DLL (#14976) (#14983)
* Sign DLLs

* Only sign vulkan-1
2021-04-06 10:34:38 -07:00
Charles Gagnon
d12a7b81fd Port 43db30d1da (#14986)
Co-authored-by: Maddy <12754347+MaddyDev@users.noreply.github.com>
2021-04-06 10:13:35 -07:00
Alan Ren
3eb705e77b redraw table header (#14963) (#14988) 2021-04-05 19:49:07 -07:00
Charles Gagnon
b421b19b73 Arc updates for March release (#14970) (#14974)
* Updated Postgres Spec for where to find engine version, removed calling calling -ev in edit commands (#14735)

* Added spec.engine.version, took out calling engine version with edit calls

* Added text wrong place

* missed updates

* PR fix

* Update Arc Postgres troubleshooting notebook

Co-authored-by: Brian Bergeron <brberger@microsoft.com>

* Remove AzdataSession from azdata commands (#14856)

* remove session

* Add in controller-context support

* Revert "Add in controller-context support"

This reverts commit 3b39b968efbf6054041cb01cb2d8443532643a82.

* Add azdataContext to login

* Undo book change

* Undo change correctly

* Add controller context support (#14862)

* remove session

* Add in controller-context support

* Add params to fake

* Fix tests

* Add info and placeholder for controller URL/name (#14887)

* Add info and placeholder for controller URL

* add period + update name

* update memento and allow editing of namespace/URL

* vBump

* vBump

* Fix tests

Co-authored-by: nasc17 <69922333+nasc17@users.noreply.github.com>
Co-authored-by: Brian Bergeron <brian.e.bergeron@gmail.com>
Co-authored-by: Brian Bergeron <brberger@microsoft.com>

Co-authored-by: nasc17 <69922333+nasc17@users.noreply.github.com>
Co-authored-by: Brian Bergeron <brian.e.bergeron@gmail.com>
Co-authored-by: Brian Bergeron <brberger@microsoft.com>
2021-04-05 13:00:26 -07:00
Charles Gagnon
124f7ca887 Remove survey link (#14971) 2021-04-05 12:49:19 -07:00
120 changed files with 1233 additions and 1109 deletions

View File

@@ -151,7 +151,7 @@ steps:
inputs: inputs:
ConnectedServiceName: 'Code Signing' ConnectedServiceName: 'Code Signing'
FolderPath: '$(agent.builddirectory)/azuredatastudio-win32-x64' FolderPath: '$(agent.builddirectory)/azuredatastudio-win32-x64'
Pattern: '*.exe,*.node,resources/app/node_modules.asar.unpacked/*.dll,swiftshader/*.dll,d3dcompiler_47.dll,libGLESv2.dll,ffmpeg.dll,libEGL.dll,Microsoft.SqlTools.Hosting.dll,Microsoft.SqlTools.ResourceProvider.Core.dll,Microsoft.SqlTools.ResourceProvider.DefaultImpl.dll,MicrosoftSqlToolsCredentials.dll,MicrosoftSqlToolsServiceLayer.dll,Newtonsoft.Json.dll,SqlSerializationService.dll,SqlToolsResourceProviderService.dll,Microsoft.SqlServer.*.dll,Microsoft.Data.Tools.Sql.BatchParser.dll' Pattern: '*.exe,*.node,resources/app/node_modules.asar.unpacked/*.dll,swiftshader/*.dll,d3dcompiler_47.dll,vulkan-1.dll,libGLESv2.dll,ffmpeg.dll,libEGL.dll,Microsoft.SqlTools.Hosting.dll,Microsoft.SqlTools.ResourceProvider.Core.dll,Microsoft.SqlTools.ResourceProvider.DefaultImpl.dll,MicrosoftSqlToolsCredentials.dll,MicrosoftSqlToolsServiceLayer.dll,Newtonsoft.Json.dll,SqlSerializationService.dll,SqlToolsResourceProviderService.dll,Microsoft.SqlServer.*.dll,Microsoft.Data.Tools.Sql.BatchParser.dll'
signConfigType: inlineSignParams signConfigType: inlineSignParams
inlineOperation: | inlineOperation: |
[ [

View File

@@ -114,6 +114,8 @@ const indentationFilter = [
'!extensions/big-data-cluster/src/bigDataCluster/controller/apiGenerated.ts', '!extensions/big-data-cluster/src/bigDataCluster/controller/apiGenerated.ts',
'!extensions/big-data-cluster/src/bigDataCluster/controller/clusterApiGenerated2.ts', '!extensions/big-data-cluster/src/bigDataCluster/controller/clusterApiGenerated2.ts',
'!resources/linux/snap/electron-launch', '!resources/linux/snap/electron-launch',
'!extensions/markdown-language-features/media/*.js',
'!extensions/simple-browser/media/*.js',
'!resources/xlf/LocProject.json', // {{SQL CARBON EDIT}} '!resources/xlf/LocProject.json', // {{SQL CARBON EDIT}}
'!build/**/*' // {{SQL CARBON EDIT}} '!build/**/*' // {{SQL CARBON EDIT}}
]; ];

View File

@@ -1,2 +1 @@
title: Azure Arc Data Services title: Azure Arc Data Services
description: A collection of notebooks to support Azure Arc Data Services.

View File

@@ -1,12 +1,10 @@
- title: Welcome - title: Welcome
url: /readme url: /readme
not_numbered: true not_numbered: true
- title: Search
search: true
- title: Postgres
url: /postgres/readme
not_numbered: true
expand_sections: true
sections: sections:
- title: TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter - title: Postgres
url: postgres/tsg100-troubleshoot-postgres url: /postgres/readme
not_numbered: true
sections:
- title: TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter
url: postgres/tsg100-troubleshoot-postgres

View File

@@ -2,6 +2,10 @@
- This chapter contains notebooks for troubleshooting Postgres on Azure Arc - This chapter contains notebooks for troubleshooting Postgres on Azure Arc
## Notebooks in this Chapter
- [TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter](tsg100-troubleshoot-postgres.ipynb)
[Home](../readme.md)
## Notebooks in this Chapter
- [TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter](../postgres/tsg100-troubleshoot-postgres.ipynb)

View File

@@ -1,7 +0,0 @@
- title: Postgres
url: /postgres/readme
not_numbered: true
expand_sections: true
sections:
- title: TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter
url: postgres/tsg100-troubleshoot-postgres

View File

@@ -2,7 +2,11 @@
"cells": [ "cells": [
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [ "source": [
"TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter\n", "TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter\n",
"===================================================================\n", "===================================================================\n",
@@ -35,14 +39,17 @@
"# the user will be prompted to select a server.\n", "# the user will be prompted to select a server.\n",
"namespace = os.environ.get('POSTGRES_SERVER_NAMESPACE')\n", "namespace = os.environ.get('POSTGRES_SERVER_NAMESPACE')\n",
"name = os.environ.get('POSTGRES_SERVER_NAME')\n", "name = os.environ.get('POSTGRES_SERVER_NAME')\n",
"version = os.environ.get('POSTGRES_SERVER_VERSION')\n",
"\n", "\n",
"tail_lines = 50" "tail_lines = 50"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [ "source": [
"### Common functions\n", "### Common functions\n",
"\n", "\n",
@@ -63,7 +70,6 @@
"import sys\n", "import sys\n",
"import os\n", "import os\n",
"import re\n", "import re\n",
"import json\n",
"import platform\n", "import platform\n",
"import shlex\n", "import shlex\n",
"import shutil\n", "import shutil\n",
@@ -76,11 +82,7 @@
"error_hints = {} # Output in stderr where a known SOP/TSG exists which will be HINTed for further help\n", "error_hints = {} # Output in stderr where a known SOP/TSG exists which will be HINTed for further help\n",
"install_hint = {} # The SOP to help install the executable if it cannot be found\n", "install_hint = {} # The SOP to help install the executable if it cannot be found\n",
"\n", "\n",
"first_run = True\n", "def run(cmd, return_output=False, no_output=False, retry_count=0, base64_decode=False, return_as_json=False):\n",
"rules = None\n",
"debug_logging = False\n",
"\n",
"def run(cmd, return_output=False, no_output=False, retry_count=0):\n",
" \"\"\"Run shell command, stream stdout, print stderr and optionally return output\n", " \"\"\"Run shell command, stream stdout, print stderr and optionally return output\n",
"\n", "\n",
" NOTES:\n", " NOTES:\n",
@@ -103,13 +105,6 @@
" output = \"\"\n", " output = \"\"\n",
" retry = False\n", " retry = False\n",
"\n", "\n",
" global first_run\n",
" global rules\n",
"\n",
" if first_run:\n",
" first_run = False\n",
" rules = load_rules()\n",
"\n",
" # When running `azdata sql query` on Windows, replace any \\n in \"\"\" strings, with \" \", otherwise we see:\n", " # When running `azdata sql query` on Windows, replace any \\n in \"\"\" strings, with \" \", otherwise we see:\n",
" #\n", " #\n",
" # ('HY090', '[HY090] [Microsoft][ODBC Driver Manager] Invalid string or buffer length (0) (SQLExecDirectW)')\n", " # ('HY090', '[HY090] [Microsoft][ODBC Driver Manager] Invalid string or buffer length (0) (SQLExecDirectW)')\n",
@@ -172,7 +167,12 @@
" if which_binary == None:\n", " if which_binary == None:\n",
" which_binary = shutil.which(cmd_actual[0])\n", " which_binary = shutil.which(cmd_actual[0])\n",
"\n", "\n",
" # Display an install HINT, so the user can click on a SOP to install the missing binary\n",
" #\n",
" if which_binary == None:\n", " if which_binary == None:\n",
" print(f\"The path used to search for '{cmd_actual[0]}' was:\")\n",
" print(sys.path)\n",
"\n",
" if user_provided_exe_name in install_hint and install_hint[user_provided_exe_name] is not None:\n", " if user_provided_exe_name in install_hint and install_hint[user_provided_exe_name] is not None:\n",
" display(Markdown(f'HINT: Use [{install_hint[user_provided_exe_name][0]}]({install_hint[user_provided_exe_name][1]}) to resolve this issue.'))\n", " display(Markdown(f'HINT: Use [{install_hint[user_provided_exe_name][0]}]({install_hint[user_provided_exe_name][1]}) to resolve this issue.'))\n",
"\n", "\n",
@@ -219,8 +219,6 @@
" break # otherwise infinite hang, have not worked out why yet.\n", " break # otherwise infinite hang, have not worked out why yet.\n",
" else:\n", " else:\n",
" print(line, end='')\n", " print(line, end='')\n",
" if rules is not None:\n",
" apply_expert_rules(line)\n",
"\n", "\n",
" if wait:\n", " if wait:\n",
" p.wait()\n", " p.wait()\n",
@@ -276,25 +274,22 @@
" if line_decoded.find(error_hint[0]) != -1:\n", " if line_decoded.find(error_hint[0]) != -1:\n",
" display(Markdown(f'HINT: Use [{error_hint[1]}]({error_hint[2]}) to resolve this issue.'))\n", " display(Markdown(f'HINT: Use [{error_hint[1]}]({error_hint[2]}) to resolve this issue.'))\n",
"\n", "\n",
" # apply expert rules (to run follow-on notebooks), based on output\n",
" #\n",
" if rules is not None:\n",
" apply_expert_rules(line_decoded)\n",
"\n",
" # Verify if a transient error, if so automatically retry (recursive)\n", " # Verify if a transient error, if so automatically retry (recursive)\n",
" #\n", " #\n",
" if user_provided_exe_name in retry_hints:\n", " if user_provided_exe_name in retry_hints:\n",
" for retry_hint in retry_hints[user_provided_exe_name]:\n", " for retry_hint in retry_hints[user_provided_exe_name]:\n",
" if line_decoded.find(retry_hint) != -1:\n", " if line_decoded.find(retry_hint) != -1:\n",
" if retry_count < MAX_RETRIES:\n", " if retry_count \u003c MAX_RETRIES:\n",
" print(f\"RETRY: {retry_count} (due to: {retry_hint})\")\n", " print(f\"RETRY: {retry_count} (due to: {retry_hint})\")\n",
" retry_count = retry_count + 1\n", " retry_count = retry_count + 1\n",
" output = run(cmd, return_output=return_output, retry_count=retry_count)\n", " output = run(cmd, return_output=return_output, retry_count=retry_count)\n",
"\n", "\n",
" if return_output:\n", " if return_output:\n",
" return output\n", " if base64_decode:\n",
" else:\n", " import base64\n",
" return\n", " return base64.b64decode(output).decode('utf-8')\n",
" else:\n",
" return output\n",
"\n", "\n",
" elapsed = datetime.datetime.now().replace(microsecond=0) - start_time\n", " elapsed = datetime.datetime.now().replace(microsecond=0) - start_time\n",
"\n", "\n",
@@ -311,78 +306,31 @@
" print(f'\\nSUCCESS: {elapsed}s elapsed.\\n')\n", " print(f'\\nSUCCESS: {elapsed}s elapsed.\\n')\n",
"\n", "\n",
" if return_output:\n", " if return_output:\n",
" return output\n", " if base64_decode:\n",
"\n", " import base64\n",
"def load_json(filename):\n", " return base64.b64decode(output).decode('utf-8')\n",
" \"\"\"Load a json file from disk and return the contents\"\"\"\n", " else:\n",
"\n", " return output\n",
" with open(filename, encoding=\"utf8\") as json_file:\n",
" return json.load(json_file)\n",
"\n",
"def load_rules():\n",
" \"\"\"Load any 'expert rules' from the metadata of this notebook (.ipynb) that should be applied to the stderr of the running executable\"\"\"\n",
"\n",
" # Load this notebook as json to get access to the expert rules in the notebook metadata.\n",
" #\n",
" try:\n",
" j = load_json(\"tsg100-troubleshoot-postgres.ipynb\")\n",
" except:\n",
" pass # If the user has renamed the book, we can't load ourself. NOTE: Is there a way in Jupyter, to know your own filename?\n",
" else:\n",
" if \"metadata\" in j and \\\n",
" \"azdata\" in j[\"metadata\"] and \\\n",
" \"expert\" in j[\"metadata\"][\"azdata\"] and \\\n",
" \"expanded_rules\" in j[\"metadata\"][\"azdata\"][\"expert\"]:\n",
"\n",
" rules = j[\"metadata\"][\"azdata\"][\"expert\"][\"expanded_rules\"]\n",
"\n",
" rules.sort() # Sort rules, so they run in priority order (the [0] element). Lowest value first.\n",
"\n",
" # print (f\"EXPERT: There are {len(rules)} rules to evaluate.\")\n",
"\n",
" return rules\n",
"\n",
"def apply_expert_rules(line):\n",
" \"\"\"Determine if the stderr line passed in, matches the regular expressions for any of the 'expert rules', if so\n",
" inject a 'HINT' to the follow-on SOP/TSG to run\"\"\"\n",
"\n",
" global rules\n",
"\n",
" for rule in rules:\n",
" notebook = rule[1]\n",
" cell_type = rule[2]\n",
" output_type = rule[3] # i.e. stream or error\n",
" output_type_name = rule[4] # i.e. ename or name \n",
" output_type_value = rule[5] # i.e. SystemExit or stdout\n",
" details_name = rule[6] # i.e. evalue or text \n",
" expression = rule[7].replace(\"\\\\*\", \"*\") # Something escaped *, and put a \\ in front of it!\n",
"\n",
" if debug_logging:\n",
" print(f\"EXPERT: If rule '{expression}' satisfied', run '{notebook}'.\")\n",
"\n",
" if re.match(expression, line, re.DOTALL):\n",
"\n",
" if debug_logging:\n",
" print(\"EXPERT: MATCH: name = value: '{0}' = '{1}' matched expression '{2}', therefore HINT '{4}'\".format(output_type_name, output_type_value, expression, notebook))\n",
"\n",
" match_found = True\n",
"\n",
" display(Markdown(f'HINT: Use [{notebook}]({notebook}) to resolve this issue.'))\n",
"\n", "\n",
"\n", "\n",
"\n", "\n",
"print('Common functions defined successfully.')\n", "# Hints for tool retry (on transient fault), known errors and install guide\n",
"\n",
"# Hints for binary (transient fault) retry, (known) error and install guide\n",
"#\n", "#\n",
"retry_hints = {'kubectl': ['A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond']}\n", "retry_hints = {}\n",
"error_hints = {'kubectl': [['no such host', 'TSG010 - Get configuration contexts', '../monitor-k8s/tsg010-get-kubernetes-contexts.ipynb'], ['No connection could be made because the target machine actively refused it', 'TSG056 - Kubectl fails with No connection could be made because the target machine actively refused it', '../repair/tsg056-kubectl-no-connection-could-be-made.ipynb']]}\n", "error_hints = {}\n",
"install_hint = {'kubectl': ['SOP036 - Install kubectl command line interface', '../install/sop036-install-kubectl.ipynb']}" "install_hint = {}\n",
"\n",
"\n",
"print('Common functions defined successfully.')"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [ "source": [
"### Get Postgres server" "### Get Postgres server"
] ]
@@ -400,10 +348,11 @@
"# Sets the 'server' variable to the spec of the Postgres server\n", "# Sets the 'server' variable to the spec of the Postgres server\n",
"\n", "\n",
"import math\n", "import math\n",
"import json\n",
"\n", "\n",
"# If a server was provided, get it\n", "# If a server was provided, get it\n",
"if namespace and name and version:\n", "if namespace and name:\n",
" server = json.loads(run(f'kubectl get postgresql-{version} -n {namespace} {name} -o json', return_output=True))\n", " server = json.loads(run(f'kubectl get postgresqls -n {namespace} {name} -o json', return_output=True))\n",
"else:\n", "else:\n",
" # Otherwise prompt the user to select a server\n", " # Otherwise prompt the user to select a server\n",
" servers = json.loads(run(f'kubectl get postgresqls --all-namespaces -o json', return_output=True))['items']\n", " servers = json.loads(run(f'kubectl get postgresqls --all-namespaces -o json', return_output=True))['items']\n",
@@ -415,19 +364,18 @@
"\n", "\n",
" pad = math.floor(math.log10(len(servers)) + 1) + 3\n", " pad = math.floor(math.log10(len(servers)) + 1) + 3\n",
" for i, s in enumerate(servers):\n", " for i, s in enumerate(servers):\n",
" print(f'{f\"[{i+1}]\":<{pad}}{full_name(s)}')\n", " print(f'{f\"[{i+1}]\":\u003c{pad}}{full_name(s)}')\n",
"\n", "\n",
" while True:\n", " while True:\n",
" try:\n", " try:\n",
" i = int(input('Enter the index of a server to troubleshoot: '))\n", " i = int(input('Enter the index of a server'))\n",
" except ValueError:\n", " except ValueError:\n",
" continue\n", " continue\n",
"\n", "\n",
" if i >= 1 and i <= len(servers):\n", " if i \u003e= 1 and i \u003c= len(servers):\n",
" server = servers[i-1]\n", " server = servers[i-1]\n",
" namespace = server['metadata']['namespace']\n", " namespace = server['metadata']['namespace']\n",
" name = server['metadata']['name']\n", " name = server['metadata']['name']\n",
" version = server['kind'][len('postgresql-'):]\n",
" break\n", " break\n",
"\n", "\n",
"display(Markdown(f'#### Got server {namespace}.{name}'))" "display(Markdown(f'#### Got server {namespace}.{name}'))"
@@ -435,7 +383,11 @@
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [ "source": [
"### Summarize all resources" "### Summarize all resources"
] ]
@@ -443,13 +395,15 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"metadata": {}, "metadata": {
"tags": []
},
"outputs": [], "outputs": [],
"source": [ "source": [
"uid = server['metadata']['uid']\n", "uid = server['metadata']['uid']\n",
"\n", "\n",
"display(Markdown(f'#### Server summary'))\n", "display(Markdown(f'#### Server summary'))\n",
"run(f'kubectl get postgresql-{version} -n {namespace} {name}')\n", "run(f'kubectl get postgresqls -n {namespace} {name}')\n",
"\n", "\n",
"display(Markdown(f'#### Resource summary'))\n", "display(Markdown(f'#### Resource summary'))\n",
"run(f'kubectl get sts,pods,pvc,svc,ep -n {namespace} -l postgresqls.arcdata.microsoft.com/cluster-id={uid}')" "run(f'kubectl get sts,pods,pvc,svc,ep -n {namespace} -l postgresqls.arcdata.microsoft.com/cluster-id={uid}')"
@@ -457,7 +411,11 @@
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [ "source": [
"### Troubleshoot the server" "### Troubleshoot the server"
] ]
@@ -465,16 +423,22 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"metadata": {}, "metadata": {
"tags": []
},
"outputs": [], "outputs": [],
"source": [ "source": [
"display(Markdown(f'#### Troubleshooting server {namespace}.{name}'))\n", "display(Markdown(f'#### Troubleshooting server {namespace}.{name}'))\n",
"run(f'kubectl describe postgresql-{version} -n {namespace} {name}')" "run(f'kubectl describe postgresqls -n {namespace} {name}')"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [ "source": [
"### Troubleshoot the pods" "### Troubleshoot the pods"
] ]
@@ -482,7 +446,9 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"metadata": {}, "metadata": {
"tags": []
},
"outputs": [], "outputs": [],
"source": [ "source": [
"pods = json.loads(run(f'kubectl get pods -n {namespace} -l postgresqls.arcdata.microsoft.com/cluster-id={uid} -o json', return_output=True))['items']\n", "pods = json.loads(run(f'kubectl get pods -n {namespace} -l postgresqls.arcdata.microsoft.com/cluster-id={uid} -o json', return_output=True))['items']\n",
@@ -505,7 +471,11 @@
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [ "source": [
"### Troubleshoot the containers" "### Troubleshoot the containers"
] ]
@@ -513,7 +483,9 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"metadata": {}, "metadata": {
"tags": []
},
"outputs": [], "outputs": [],
"source": [ "source": [
"# Summarize and get logs from each container\n", "# Summarize and get logs from each container\n",
@@ -521,7 +493,7 @@
" pod_name = pod['metadata']['name']\n", " pod_name = pod['metadata']['name']\n",
" cons = pod['spec']['containers']\n", " cons = pod['spec']['containers']\n",
" con_statuses = pod['status'].get('containerStatuses', [])\n", " con_statuses = pod['status'].get('containerStatuses', [])\n",
" display(Markdown(f'#### Troubleshooting {len(cons)} container{\"\" if len(cons) < 2 else \"s\"} '\n", " display(Markdown(f'#### Troubleshooting {len(cons)} container{\"\" if len(cons) \u003c 2 else \"s\"} '\n",
" f'containers for pod {namespace}.{pod_name}'))\n", " f'containers for pod {namespace}.{pod_name}'))\n",
"\n", "\n",
" for i, con in enumerate(cons):\n", " for i, con in enumerate(cons):\n",
@@ -537,14 +509,18 @@
" run(f'kubectl logs -n {namespace} {pod_name} {con_name} --tail {tail_lines}')\n", " run(f'kubectl logs -n {namespace} {pod_name} {con_name} --tail {tail_lines}')\n",
"\n", "\n",
" # Get logs from the previous terminated container if one exists\n", " # Get logs from the previous terminated container if one exists\n",
" if con_restarts > 0:\n", " if con_restarts \u003e 0:\n",
" display(Markdown(f'#### Logs from previous terminated container {namespace}.{pod_name}/{con_name}'))\n", " display(Markdown(f'#### Logs from previous terminated container {namespace}.{pod_name}/{con_name}'))\n",
" run(f'kubectl logs -n {namespace} {pod_name} {con_name} --tail {tail_lines} --previous')" " run(f'kubectl logs -n {namespace} {pod_name} {con_name} --tail {tail_lines} --previous')"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {}, "execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [ "source": [
"### Troubleshoot the PersistentVolumeClaims" "### Troubleshoot the PersistentVolumeClaims"
] ]
@@ -552,7 +528,9 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"metadata": {}, "metadata": {
"tags": []
},
"outputs": [], "outputs": [],
"source": [ "source": [
"display(Markdown(f'#### Troubleshooting PersistentVolumeClaims'))\n", "display(Markdown(f'#### Troubleshooting PersistentVolumeClaims'))\n",
@@ -562,10 +540,12 @@
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"metadata": {}, "metadata": {
"tags": []
},
"outputs": [], "outputs": [],
"source": [ "source": [
"print('Notebook execution complete.')" "print(\"Notebook execution is complete.\")"
] ]
} }
], ],
@@ -576,20 +556,36 @@
"name": "python3", "name": "python3",
"display_name": "Python 3" "display_name": "Python 3"
}, },
"azdata": { "pansop": {
"related": "",
"test": { "test": {
"ci": false, "strategy": "",
"gci": false "types": null,
}, "disable": {
"contract": { "reason": "",
"requires": { "workitems": null,
"kubectl": { "types": null
"installed": true
}
} }
}, },
"side_effects": false "target": {
} "current": "public",
"final": "public"
},
"internal": {
"parameters": null,
"symlink": false
},
"timeout": "0"
},
"language_info": {
"codemirror_mode": "{ Name: \"\", Version: \"\"}",
"file_extension": "",
"mimetype": "",
"name": "",
"nbconvert_exporter": "",
"pygments_lexer": "",
"version": ""
},
"widgets": []
} }
} }

View File

@@ -1,5 +0,0 @@
# Azure Arc Data Services Jupyter Book
## Chapters
1. [Postgres](postgres/readme.md) - notebooks for troubleshooting Postgres on Azure Arc.

View File

@@ -2,14 +2,14 @@
"name": "arc", "name": "arc",
"displayName": "%arc.displayName%", "displayName": "%arc.displayName%",
"description": "%arc.description%", "description": "%arc.description%",
"version": "0.9.0", "version": "0.9.3",
"publisher": "Microsoft", "publisher": "Microsoft",
"preview": true, "preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
"icon": "images/extension.png", "icon": "images/extension.png",
"engines": { "engines": {
"vscode": "*", "vscode": "*",
"azdata": ">=1.27.0" "azdata": ">=1.28.0"
}, },
"activationEvents": [ "activationEvents": [
"onCommand:arc.connectToController", "onCommand:arc.connectToController",
@@ -520,7 +520,7 @@
}, },
{ {
"name": "azdata", "name": "azdata",
"version": "20.3.1" "version": "20.3.2"
} }
], ],
"when": true "when": true
@@ -772,7 +772,7 @@
}, },
{ {
"name": "azdata", "name": "azdata",
"version": "20.3.1" "version": "20.3.2"
} }
], ],
"when": "true" "when": "true"
@@ -1001,7 +1001,7 @@
}, },
{ {
"name": "azdata", "name": "azdata",
"version": "20.3.1" "version": "20.3.2"
} }
], ],
"when": "mi-type=arc-mi" "when": "mi-type=arc-mi"

View File

@@ -10,6 +10,7 @@ import * as loc from '../localizedConstants';
import { throwUnless } from './utils'; import { throwUnless } from './utils';
export interface KubeClusterContext { export interface KubeClusterContext {
name: string; name: string;
namespace?: string;
isCurrentContext: boolean; isCurrentContext: boolean;
} }
@@ -18,7 +19,7 @@ export interface KubeClusterContext {
* *
* @param configFile * @param configFile
*/ */
export function getKubeConfigClusterContexts(configFile: string): Promise<KubeClusterContext[]> { export function getKubeConfigClusterContexts(configFile: string): KubeClusterContext[] {
const config: any = yamljs.load(configFile); const config: any = yamljs.load(configFile);
const rawContexts = <any[]>config['contexts']; const rawContexts = <any[]>config['contexts'];
throwUnless(rawContexts && rawContexts.length, loc.noContextFound(configFile)); throwUnless(rawContexts && rawContexts.length, loc.noContextFound(configFile));
@@ -26,16 +27,16 @@ export function getKubeConfigClusterContexts(configFile: string): Promise<KubeCl
throwUnless(currentContext, loc.noCurrentContextFound(configFile)); throwUnless(currentContext, loc.noCurrentContextFound(configFile));
const contexts: KubeClusterContext[] = []; const contexts: KubeClusterContext[] = [];
rawContexts.forEach(rawContext => { rawContexts.forEach(rawContext => {
const name = <string>rawContext['name']; const name = rawContext.name as string;
const namespace = rawContext.context.namespace as string;
throwUnless(name, loc.noNameInContext(configFile)); throwUnless(name, loc.noNameInContext(configFile));
if (name) { contexts.push({
contexts.push({ name: name,
name: name, namespace: namespace,
isCurrentContext: name === currentContext isCurrentContext: name === currentContext
}); });
}
}); });
return Promise.resolve(contexts); return contexts;
} }
/** /**
@@ -47,22 +48,23 @@ export function getKubeConfigClusterContexts(configFile: string): Promise<KubeCl
* *
* *
* @param clusterContexts * @param clusterContexts
* @param previousClusterContext * @param previousClusterContextName
* @param throwIfNotFound * @param throwIfNotFound
*/ */
export function getCurrentClusterContext(clusterContexts: KubeClusterContext[], previousClusterContext?: string, throwIfNotFound: boolean = false): string { export function getCurrentClusterContext(clusterContexts: KubeClusterContext[], previousClusterContextName?: string, throwIfNotFound: boolean = false): KubeClusterContext {
if (previousClusterContext) { if (previousClusterContextName) {
if (clusterContexts.find(c => c.name === previousClusterContext)) { // if previous cluster context value is found in clusters then return that value const previousClusterContext = clusterContexts.find(c => c.name === previousClusterContextName);
if (previousClusterContext) { // if previous cluster context value is found in clusters then return that value
return previousClusterContext; return previousClusterContext;
} else { } else {
if (throwIfNotFound) { if (throwIfNotFound) {
throw new Error(loc.clusterContextNotFound(previousClusterContext)); throw new Error(loc.clusterContextNotFound(previousClusterContextName));
} }
} }
} }
// if not previousClusterContext or throwIfNotFound was false when previousCLusterContext was not found in the clusterContexts // if not previousClusterContext or throwIfNotFound was false when previousCLusterContext was not found in the clusterContexts
const currentClusterContext = clusterContexts.find(c => c.isCurrentContext)?.name; const currentClusterContext = clusterContexts.find(c => c.isCurrentContext);
throwUnless(currentClusterContext !== undefined, loc.noCurrentClusterContext); throwUnless(currentClusterContext !== undefined, loc.noCurrentClusterContext);
return currentClusterContext; return currentClusterContext;
} }

View File

@@ -97,13 +97,18 @@ export function connectToMSSql(name: string): string { return localize('arc.conn
export function connectToPGSql(name: string): string { return localize('arc.connectToPGSql', "Connect to PostgreSQL Hyperscale - Azure Arc ({0})", name); } export function connectToPGSql(name: string): string { return localize('arc.connectToPGSql', "Connect to PostgreSQL Hyperscale - Azure Arc ({0})", name); }
export const passwordToController = localize('arc.passwordToController', "Provide Password to Controller"); export const passwordToController = localize('arc.passwordToController', "Provide Password to Controller");
export const controllerUrl = localize('arc.controllerUrl', "Controller URL"); export const controllerUrl = localize('arc.controllerUrl', "Controller URL");
export const controllerUrlPlaceholder = localize('arc.controllerUrlPlaceholder', "https://<IP or hostname>:<port>");
export const controllerUrlDescription = localize('arc.controllerUrlDescription', "The Controller URL is necessary if there are multiple clusters with the same namespace - this should generally not be necessary.");
export const serverEndpoint = localize('arc.serverEndpoint', "Server Endpoint"); export const serverEndpoint = localize('arc.serverEndpoint', "Server Endpoint");
export const controllerName = localize('arc.controllerName', "Name"); export const controllerName = localize('arc.controllerName', "Name");
export const controllerNameDescription = localize('arc.controllerNameDescription', "The name to display in the tree view, this is not applied to the controller itself.");
export const controllerKubeConfig = localize('arc.controllerKubeConfig', "Kube Config File Path"); export const controllerKubeConfig = localize('arc.controllerKubeConfig', "Kube Config File Path");
export const controllerClusterContext = localize('arc.controllerClusterContext', "Cluster Context"); export const controllerClusterContext = localize('arc.controllerClusterContext', "Cluster Context");
export const defaultControllerName = localize('arc.defaultControllerName', "arc-dc"); export const defaultControllerName = localize('arc.defaultControllerName', "arc-dc");
export const postgresProviderName = localize('arc.postgresProviderName', "PGSQL"); export const postgresProviderName = localize('arc.postgresProviderName', "PGSQL");
export const miaaProviderName = localize('arc.miaaProviderName', "MSSQL"); export const miaaProviderName = localize('arc.miaaProviderName', "MSSQL");
export const controllerUsername = localize('arc.controllerUsername', "Controller Username");
export const controllerPassword = localize('arc.controllerPassword', "Controller Password");
export const username = localize('arc.username', "Username"); export const username = localize('arc.username', "Username");
export const password = localize('arc.password', "Password"); export const password = localize('arc.password', "Password");
export const rememberPassword = localize('arc.rememberPassword', "Remember Password"); export const rememberPassword = localize('arc.rememberPassword', "Remember Password");

View File

@@ -46,6 +46,20 @@ export class ControllerModel {
return this._info; return this._info;
} }
/**
* Gets the controller context to use when executing azdata commands. This is in one of two forms :
*
* If no URL is specified for this controller then just the namespace is used (e.g. test-namespace)
* If a URL is specified then a 3-part name is used, combining the namespace, username and URL separated by
* / (e.g. test-namespace/admin/https://10.91.86.13:30080)
*/
public get controllerContext(): string {
if (this._info.endpoint) {
return `${this._info.namespace}/${this._info.username}/${this._info.endpoint}`;
}
return this._info.namespace;
}
public set info(value: ControllerInfo) { public set info(value: ControllerInfo) {
this._info = value; this._info = value;
this._onInfoUpdated.fire(this._info); this._onInfoUpdated.fire(this._info);
@@ -63,10 +77,10 @@ export class ControllerModel {
* calls from changing the context while commands for this session are being executed. * calls from changing the context while commands for this session are being executed.
* @param promptReconnect * @param promptReconnect
*/ */
public async acquireAzdataSession(promptReconnect: boolean = false): Promise<azdataExt.AzdataSession> { public async login(promptReconnect: boolean = false): Promise<void> {
let promptForValidClusterContext: boolean = false; let promptForValidClusterContext: boolean = false;
try { try {
const contexts = await getKubeConfigClusterContexts(this.info.kubeConfigFilePath); const contexts = getKubeConfigClusterContexts(this.info.kubeConfigFilePath);
getCurrentClusterContext(contexts, this.info.kubeClusterContext, true); // this throws if this.info.kubeClusterContext is not found in 'contexts' getCurrentClusterContext(contexts, this.info.kubeClusterContext, true); // this throws if this.info.kubeClusterContext is not found in 'contexts'
} catch (error) { } catch (error) {
const response = await vscode.window.showErrorMessage(loc.clusterContextConfigNoLongerValid(this.info.kubeConfigFilePath, this.info.kubeClusterContext, error), loc.yes, loc.no); const response = await vscode.window.showErrorMessage(loc.clusterContextConfigNoLongerValid(this.info.kubeConfigFilePath, this.info.kubeClusterContext, error), loc.yes, loc.no);
@@ -100,8 +114,7 @@ export class ControllerModel {
} }
} }
} }
await this._azdataApi.azdata.login({ endpoint: this.info.endpoint, namespace: this.info.namespace }, this.info.username, this._password, this.azdataAdditionalEnvVars);
return this._azdataApi.azdata.acquireSession(this.info.url, this.info.username, this._password, this.azdataAdditionalEnvVars);
} }
/** /**
@@ -115,67 +128,64 @@ export class ControllerModel {
await this.refresh(false); await this.refresh(false);
} }
} }
public async refresh(showErrors: boolean = true, promptReconnect: boolean = false): Promise<void> { public async refresh(showErrors: boolean = true): Promise<void> {
const session = await this.acquireAzdataSession(promptReconnect); // First need to log in to ensure that we're able to authenticate with the controller
await this.login(false);
const newRegistrations: Registration[] = []; const newRegistrations: Registration[] = [];
try { await Promise.all([
await Promise.all([ this._azdataApi.azdata.arc.dc.config.show(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
this._azdataApi.azdata.arc.dc.config.show(this.azdataAdditionalEnvVars, session).then(result => { this._controllerConfig = result.result;
this._controllerConfig = result.result; this.configLastUpdated = new Date();
this.configLastUpdated = new Date(); this._onConfigUpdated.fire(this._controllerConfig);
this._onConfigUpdated.fire(this._controllerConfig); }).catch(err => {
}).catch(err => { // If an error occurs show a message so the user knows something failed but still
// If an error occurs show a message so the user knows something failed but still // fire the event so callers hooking into this can handle the error (e.g. so dashboards don't show the
// fire the event so callers hooking into this can handle the error (e.g. so dashboards don't show the // loading icon forever)
// loading icon forever) if (showErrors) {
if (showErrors) { vscode.window.showErrorMessage(loc.fetchConfigFailed(this.info.name, err));
vscode.window.showErrorMessage(loc.fetchConfigFailed(this.info.name, err)); }
} this._onConfigUpdated.fire(this._controllerConfig);
this._onConfigUpdated.fire(this._controllerConfig); throw err;
throw err; }),
this._azdataApi.azdata.arc.dc.endpoint.list(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
this._endpoints = result.result;
this.endpointsLastUpdated = new Date();
this._onEndpointsUpdated.fire(this._endpoints);
}).catch(err => {
// If an error occurs show a message so the user knows something failed but still
// fire the event so callers can know to update (e.g. so dashboards don't show the
// loading icon forever)
if (showErrors) {
vscode.window.showErrorMessage(loc.fetchEndpointsFailed(this.info.name, err));
}
this._onEndpointsUpdated.fire(this._endpoints);
throw err;
}),
Promise.all([
this._azdataApi.azdata.arc.postgres.server.list(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
newRegistrations.push(...result.result.map(r => {
return {
instanceName: r.name,
state: r.state,
instanceType: ResourceType.postgresInstances
};
}));
}), }),
this._azdataApi.azdata.arc.dc.endpoint.list(this.azdataAdditionalEnvVars, session).then(result => { this._azdataApi.azdata.arc.sql.mi.list(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
this._endpoints = result.result; newRegistrations.push(...result.result.map(r => {
this.endpointsLastUpdated = new Date(); return {
this._onEndpointsUpdated.fire(this._endpoints); instanceName: r.name,
}).catch(err => { state: r.state,
// If an error occurs show a message so the user knows something failed but still instanceType: ResourceType.sqlManagedInstances
// fire the event so callers can know to update (e.g. so dashboards don't show the };
// loading icon forever) }));
if (showErrors) {
vscode.window.showErrorMessage(loc.fetchEndpointsFailed(this.info.name, err));
}
this._onEndpointsUpdated.fire(this._endpoints);
throw err;
}),
Promise.all([
this._azdataApi.azdata.arc.postgres.server.list(this.azdataAdditionalEnvVars, session).then(result => {
newRegistrations.push(...result.result.map(r => {
return {
instanceName: r.name,
state: r.state,
instanceType: ResourceType.postgresInstances
};
}));
}),
this._azdataApi.azdata.arc.sql.mi.list(this.azdataAdditionalEnvVars, session).then(result => {
newRegistrations.push(...result.result.map(r => {
return {
instanceName: r.name,
state: r.state,
instanceType: ResourceType.sqlManagedInstances
};
}));
})
]).then(() => {
this._registrations = newRegistrations;
this.registrationsLastUpdated = new Date();
this._onRegistrationsUpdated.fire(this._registrations);
}) })
]); ]).then(() => {
} finally { this._registrations = newRegistrations;
session.dispose(); this.registrationsLastUpdated = new Date();
} this._onRegistrationsUpdated.fire(this._registrations);
})
]);
} }
public get endpoints(): azdataExt.DcEndpointListResult[] { public get endpoints(): azdataExt.DcEndpointListResult[] {
@@ -204,6 +214,6 @@ export class ControllerModel {
* property to for use a display label for this controller * property to for use a display label for this controller
*/ */
public get label(): string { public get label(): string {
return `${this.info.name} (${this.info.url})`; return `${this.info.name} (${this.controllerContext})`;
} }
} }

View File

@@ -71,11 +71,9 @@ export class MiaaModel extends ResourceModel {
return this._refreshPromise.promise; return this._refreshPromise.promise;
} }
this._refreshPromise = new Deferred(); this._refreshPromise = new Deferred();
let session: azdataExt.AzdataSession | undefined = undefined;
try { try {
session = await this.controllerModel.acquireAzdataSession();
try { try {
const result = await this._azdataApi.azdata.arc.sql.mi.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, session); const result = await this._azdataApi.azdata.arc.sql.mi.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, this.controllerModel.controllerContext);
this._config = result.result; this._config = result.result;
this.configLastUpdated = new Date(); this.configLastUpdated = new Date();
this._onConfigUpdated.fire(this._config); this._onConfigUpdated.fire(this._config);
@@ -109,7 +107,6 @@ export class MiaaModel extends ResourceModel {
this._refreshPromise.reject(err); this._refreshPromise.reject(err);
throw err; throw err;
} finally { } finally {
session?.dispose();
this._refreshPromise = undefined; this._refreshPromise = undefined;
} }
} }

View File

@@ -53,10 +53,7 @@ export class PostgresModel extends ResourceModel {
/** Returns the major version of Postgres */ /** Returns the major version of Postgres */
public get engineVersion(): string | undefined { public get engineVersion(): string | undefined {
const kind = this._config?.kind; return this._config?.spec.engine.version;
return kind
? kind.substring(kind.lastIndexOf('-') + 1)
: undefined;
} }
/** Returns the IP address and port of Postgres */ /** Returns the IP address and port of Postgres */
@@ -121,10 +118,8 @@ export class PostgresModel extends ResourceModel {
return this._refreshPromise.promise; return this._refreshPromise.promise;
} }
this._refreshPromise = new Deferred(); this._refreshPromise = new Deferred();
let session: azdataExt.AzdataSession | undefined = undefined;
try { try {
session = await this.controllerModel.acquireAzdataSession(); this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, this.controllerModel.controllerContext)).result;
this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, session)).result;
this.configLastUpdated = new Date(); this.configLastUpdated = new Date();
this._onConfigUpdated.fire(this._config); this._onConfigUpdated.fire(this._config);
this._refreshPromise.resolve(); this._refreshPromise.resolve();
@@ -132,7 +127,6 @@ export class PostgresModel extends ResourceModel {
this._refreshPromise.reject(err); this._refreshPromise.reject(err);
throw err; throw err;
} finally { } finally {
session?.dispose();
this._refreshPromise = undefined; this._refreshPromise = undefined;
} }
} }

View File

@@ -30,7 +30,7 @@ export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourcePro
const controller = (await getRegisteredDataControllers(this._treeProvider)).find(ci => ci.label === controllerLabel); const controller = (await getRegisteredDataControllers(this._treeProvider)).find(ci => ci.label === controllerLabel);
throwUnless(controller !== undefined, loc.noControllerInfoFound(controllerLabel)); throwUnless(controller !== undefined, loc.noControllerInfoFound(controllerLabel));
switch (variableName) { switch (variableName) {
case 'endpoint': return controller.info.url; case 'endpoint': return controller.info.endpoint || '';
case 'username': return controller.info.username; case 'username': return controller.info.username;
case 'kubeConfig': return controller.info.kubeConfigFilePath; case 'kubeConfig': return controller.info.kubeConfigFilePath;
case 'clusterContext': return controller.info.kubeClusterContext; case 'clusterContext': return controller.info.kubeClusterContext;

View File

@@ -51,7 +51,7 @@ describe('KubeUtils', function (): void {
contexts[1].name.should.equal('kubernetes-admin@kubernetes', `test: ${testName} failed`); contexts[1].name.should.equal('kubernetes-admin@kubernetes', `test: ${testName} failed`);
contexts[1].isCurrentContext.should.be.false(`test: ${testName} failed`); contexts[1].isCurrentContext.should.be.false(`test: ${testName} failed`);
}; };
verifyContexts(await getKubeConfigClusterContexts(configFile), 'getKubeConfigClusterContexts'); verifyContexts(getKubeConfigClusterContexts(configFile), 'getKubeConfigClusterContexts');
}); });
it('throws error when unable to load config file', async () => { it('throws error when unable to load config file', async () => {
const error = new Error('unknown error accessing file'); const error = new Error('unknown error accessing file');

View File

@@ -23,9 +23,9 @@ export class FakeAzdataApi implements azdataExt.IAzdataApi {
}, },
postgres: { postgres: {
server: { server: {
postgresInstances: [], postgresInstances: <azdataExt.PostgresServerListResult[]>[],
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }, delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
async list(): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> { return <any>{ result: this.postgresInstances }; }, async list(): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> { return { result: this.postgresInstances, logs: [], stdout: [], stderr: [] }; },
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> { throw new Error('Method not implemented.'); }, show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> { throw new Error('Method not implemented.'); },
edit( edit(
_name: string, _name: string,
@@ -42,16 +42,15 @@ export class FakeAzdataApi implements azdataExt.IAzdataApi {
replaceEngineSettings?: boolean, replaceEngineSettings?: boolean,
workers?: number workers?: number
}, },
_engineVersion?: string,
_additionalEnvVars?: azdataExt.AdditionalEnvVars _additionalEnvVars?: azdataExt.AdditionalEnvVars
): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); } ): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
} }
}, },
sql: { sql: {
mi: { mi: {
miaaInstances: [], miaaInstances: <azdataExt.SqlMiListResult[]>[],
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }, delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
async list(): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> { return <any>{ result: this.miaaInstances }; }, async list(): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> { return { logs: [], stdout: [], stderr: [], result: this.miaaInstances }; },
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> { throw new Error('Method not implemented.'); }, show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> { throw new Error('Method not implemented.'); },
edit( edit(
_name: string, _name: string,
@@ -66,17 +65,14 @@ export class FakeAzdataApi implements azdataExt.IAzdataApi {
} }
}; };
// public postgresInstances: azdataExt.PostgresServerListResult[] = [];
public set postgresInstances(instances: azdataExt.PostgresServerListResult[]) { public set postgresInstances(instances: azdataExt.PostgresServerListResult[]) {
this._arcApi.postgres.server.postgresInstances = <any>instances; this._arcApi.postgres.server.postgresInstances = instances;
} }
public set miaaInstances(instances: azdataExt.SqlMiListResult[]) { public set miaaInstances(instances: azdataExt.SqlMiListResult[]) {
this._arcApi.sql.mi.miaaInstances = <any>instances; this._arcApi.sql.mi.miaaInstances = instances;
} }
// public miaaInstances: azdataExt.SqlMiListResult[] = [];
// //
// API Implementation // API Implementation
// //
@@ -86,12 +82,9 @@ export class FakeAzdataApi implements azdataExt.IAzdataApi {
getPath(): Promise<string> { getPath(): Promise<string> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
login(_endpoint: string, _username: string, _password: string): Promise<azdataExt.AzdataOutput<void>> { login(_endpointOrNamespace: azdataExt.EndpointOrNamespace, _username: string, _password: string, _additionalEnvVars: azdataExt.AdditionalEnvVars = {}, _azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> {
return <any>undefined; return <any>undefined;
} }
acquireSession(_endpoint: string, _username: string, _password: string): Promise<azdataExt.AzdataSession> {
return Promise.resolve({ dispose: () => { } });
}
version(): Promise<azdataExt.AzdataOutput<string>> { version(): Promise<azdataExt.AzdataOutput<string>> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }

View File

@@ -11,7 +11,7 @@ import { AzureArcTreeDataProvider } from '../../ui/tree/azureArcTreeDataProvider
export class FakeControllerModel extends ControllerModel { export class FakeControllerModel extends ControllerModel {
constructor(treeDataProvider?: AzureArcTreeDataProvider, info?: Partial<ControllerInfo>, password?: string) { constructor(treeDataProvider?: AzureArcTreeDataProvider, info?: Partial<ControllerInfo>, password?: string) {
const _info: ControllerInfo = Object.assign({ id: uuid(), url: '', kubeConfigFilePath: '', kubeClusterContext: '', name: '', username: '', rememberPassword: false, resources: [] }, info); const _info: ControllerInfo = Object.assign({ id: uuid(), endpoint: '', kubeConfigFilePath: '', kubeClusterContext: '', name: '', namespace: '', username: '', rememberPassword: false, resources: [] }, info);
super(treeDataProvider!, _info, password); super(treeDataProvider!, _info, password);
} }

View File

@@ -22,6 +22,20 @@ interface ExtensionGlobalMemento extends vscode.Memento {
setKeysForSync(keys: string[]): void; setKeysForSync(keys: string[]): void;
} }
function getDefaultControllerInfo(): ControllerInfo {
return {
id: uuid(),
endpoint: '127.0.0.1',
kubeConfigFilePath: '/path/to/.kube/config',
kubeClusterContext: 'currentCluster',
username: 'admin',
name: 'arc',
namespace: 'arc-ns',
rememberPassword: true,
resources: []
};
}
describe('ControllerModel', function (): void { describe('ControllerModel', function (): void {
afterEach(function (): void { afterEach(function (): void {
sinon.restore(); sinon.restore();
@@ -39,15 +53,15 @@ describe('ControllerModel', function (): void {
beforeEach(function (): void { beforeEach(function (): void {
sinon.stub(ConnectToControllerDialog.prototype, 'showDialog'); sinon.stub(ConnectToControllerDialog.prototype, 'showDialog');
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').resolves([{ name: 'currentCluster', isCurrentContext: true }]); sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').returns([{ name: 'currentCluster', isCurrentContext: true }]);
sinon.stub(vscode.window, 'showErrorMessage').resolves(<any>loc.yes); sinon.stub(vscode.window, 'showErrorMessage').resolves(<any>loc.yes);
}); });
it('Rejected with expected error when user cancels', async function (): Promise<void> { it('Rejected with expected error when user cancels', async function (): Promise<void> {
// Returning an undefined model here indicates that the dialog closed without clicking "Ok" - usually through the user clicking "Cancel" // Returning an undefined model here indicates that the dialog closed without clicking "Ok" - usually through the user clicking "Cancel"
sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(undefined)); sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(undefined));
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }); const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
await should(model.acquireAzdataSession()).be.rejectedWith(new UserCancelledError(loc.userCancelledError)); await should(model.login()).be.rejectedWith(new UserCancelledError(loc.userCancelledError));
}); });
it('Reads password from cred store', async function (): Promise<void> { it('Reads password from cred store', async function (): Promise<void> {
@@ -62,13 +76,13 @@ describe('ControllerModel', function (): void {
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>(); const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>(); const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined)); azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object); azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object }); sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }); const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
await model.acquireAzdataSession(); await model.login();
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once()); azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
}); });
it('Prompt for password when not in cred store', async function (): Promise<void> { it('Prompt for password when not in cred store', async function (): Promise<void> {
@@ -83,18 +97,18 @@ describe('ControllerModel', function (): void {
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>(); const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>(); const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined)); azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object); azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object }); sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
// Set up dialog to return new model with our password // Set up dialog to return new model with our password
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password); const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), password);
sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password })); sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }); const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
await model.acquireAzdataSession(); await model.login();
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once()); azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
}); });
it('Prompt for password when rememberPassword is true but prompt reconnect is true', async function (): Promise<void> { it('Prompt for password when rememberPassword is true but prompt reconnect is true', async function (): Promise<void> {
@@ -108,19 +122,19 @@ describe('ControllerModel', function (): void {
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>(); const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>(); const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined)); azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object); azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object }); sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
// Set up dialog to return new model with our new password from the reprompt // Set up dialog to return new model with our new password from the reprompt
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password); const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), password);
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password })); const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }); const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
await model.acquireAzdataSession(true); await model.login(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called'); should(waitForCloseStub.called).be.true('waitForClose should have been called');
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once()); azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
}); });
it('Prompt for password when we already have a password but prompt reconnect is true', async function (): Promise<void> { it('Prompt for password when we already have a password but prompt reconnect is true', async function (): Promise<void> {
@@ -134,20 +148,20 @@ describe('ControllerModel', function (): void {
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>(); const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>(); const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined)); azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object); azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object }); sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
// Set up dialog to return new model with our new password from the reprompt // Set up dialog to return new model with our new password from the reprompt
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password); const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), password);
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password })); const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
// Set up original model with a password // Set up original model with a password
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, 'originalPassword'); const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo(), 'originalPassword');
await model.acquireAzdataSession(true); await model.login(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called'); should(waitForCloseStub.called).be.true('waitForClose should have been called');
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once()); azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
}); });
it('Model values are updated correctly when modified during reconnect', async function (): Promise<void> { it('Model values are updated correctly when modified during reconnect', async function (): Promise<void> {
@@ -162,7 +176,7 @@ describe('ControllerModel', function (): void {
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>(); const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>(); const azdataMock = TypeMoq.Mock.ofType<azdataExt.IAzdataApi>();
azdataMock.setup(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined)); azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object); azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object }); sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
@@ -170,27 +184,19 @@ describe('ControllerModel', function (): void {
const originalPassword = 'originalPassword'; const originalPassword = 'originalPassword';
const model = new ControllerModel( const model = new ControllerModel(
treeDataProvider, treeDataProvider,
{ getDefaultControllerInfo(),
id: uuid(),
url: '127.0.0.1',
kubeConfigFilePath: '/path/to/.kube/config',
kubeClusterContext: 'currentCluster',
username: 'admin',
name: 'arc',
rememberPassword: false,
resources: []
},
originalPassword originalPassword
); );
await treeDataProvider.addOrUpdateController(model, originalPassword); await treeDataProvider.addOrUpdateController(model, originalPassword);
const newInfo: ControllerInfo = { const newInfo: ControllerInfo = {
id: model.info.id, // The ID stays the same since we're just re-entering information for the same model id: model.info.id, // The ID stays the same since we're just re-entering information for the same model
url: 'newUrl', endpoint: 'newUrl',
kubeConfigFilePath: '/path/to/.kube/config', kubeConfigFilePath: '/path/to/.kube/config',
kubeClusterContext: 'currentCluster', kubeClusterContext: 'currentCluster',
username: 'newUser', username: 'newUser',
name: 'newName', name: 'newName',
namespace: 'newNamespace',
rememberPassword: true, rememberPassword: true,
resources: [] resources: []
}; };
@@ -203,7 +209,7 @@ describe('ControllerModel', function (): void {
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve( const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(
{ controllerModel: newModel, password: newPassword })); { controllerModel: newModel, password: newPassword }));
await model.acquireAzdataSession(true); await model.login(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called'); should(waitForCloseStub.called).be.true('waitForClose should have been called');
should((await treeDataProvider.getChildren()).length).equal(1, 'Tree Data provider should still only have 1 node'); should((await treeDataProvider.getChildren()).length).equal(1, 'Tree Data provider should still only have 1 node');
should(model.info).deepEqual(newInfo, 'Model info should have been updated'); should(model.info).deepEqual(newInfo, 'Model info should have been updated');

View File

@@ -40,7 +40,8 @@ export const FakePostgresServerShowOutput: azdataExt.AzdataOutput<azdataExt.Post
extensions: [{ name: '' }], extensions: [{ name: '' }],
settings: { settings: {
default: { ['']: '' } default: { ['']: '' }
} },
version: ''
}, },
scale: { scale: {
shards: 0, shards: 0,
@@ -114,7 +115,7 @@ describe('PostgresModel', function (): void {
controllerModel = new FakeControllerModel(); controllerModel = new FakeControllerModel();
//Stub calling azdata login and acquiring session //Stub calling azdata login and acquiring session
sinon.stub(controllerModel, 'acquireAzdataSession').returns(Promise.resolve(vscode.Disposable.from())); sinon.stub(controllerModel, 'login').returns(Promise.resolve());
// Stub the azdata CLI API // Stub the azdata CLI API
azdataApi = new FakeAzdataApi(); azdataApi = new FakeAzdataApi();

View File

@@ -38,7 +38,8 @@ export const FakePostgresServerShowOutput: azdataExt.AzdataOutput<azdataExt.Post
extensions: [{ name: '' }], extensions: [{ name: '' }],
settings: { settings: {
default: { ['']: '' } default: { ['']: '' }
} },
version: '12'
}, },
scale: { scale: {
shards: 0, shards: 0,
@@ -121,7 +122,7 @@ describe('postgresConnectionStringsPage', function (): void {
controllerModel = new FakeControllerModel(); controllerModel = new FakeControllerModel();
//Stub calling azdata login and acquiring session //Stub calling azdata login and acquiring session
sinon.stub(controllerModel, 'acquireAzdataSession').returns(Promise.resolve(vscode.Disposable.from())); sinon.stub(controllerModel, 'login').returns(Promise.resolve());
// Setup PostgresModel // Setup PostgresModel
const postgresResource: PGResourceInfo = { name: 'pgt', resourceType: '' }; const postgresResource: PGResourceInfo = { name: 'pgt', resourceType: '' };

View File

@@ -78,7 +78,7 @@ describe('postgresOverviewPage', () => {
beforeEach(() => { beforeEach(() => {
sinon.stub(utils, 'promptForInstanceDeletion').returns(Promise.resolve(true)); sinon.stub(utils, 'promptForInstanceDeletion').returns(Promise.resolve(true));
sinon.stub(controllerModel, 'acquireAzdataSession').returns(Promise.resolve(vscode.Disposable.from())); sinon.stub(controllerModel, 'login').returns(Promise.resolve());
refreshTreeNode = sinon.stub(controllerModel, 'refreshTreeNode'); refreshTreeNode = sinon.stub(controllerModel, 'refreshTreeNode');
}); });

View File

@@ -18,8 +18,8 @@ describe('ConnectControllerDialog', function (): void {
(<{ info: ControllerInfo | undefined, description: string }[]>[ (<{ info: ControllerInfo | undefined, description: string }[]>[
{ info: undefined, description: 'all input' }, { info: undefined, description: 'all input' },
{ info: { url: '127.0.0.1' }, description: 'all but URL' }, { info: { endpoint: '127.0.0.1' }, description: 'all but URL' },
{ info: { url: '127.0.0.1', username: 'sa' }, description: 'all but URL and password' }]).forEach(test => { { info: { endpoint: '127.0.0.1', username: 'sa' }, description: 'all but URL and password' }]).forEach(test => {
it(`Validate returns false when ${test.description} is empty`, async function (): Promise<void> { it(`Validate returns false when ${test.description} is empty`, async function (): Promise<void> {
const connectControllerDialog = new ConnectToControllerDialog(undefined!); const connectControllerDialog = new ConnectToControllerDialog(undefined!);
connectControllerDialog.showDialog(test.info, undefined); connectControllerDialog.showDialog(test.info, undefined);
@@ -32,7 +32,7 @@ describe('ConnectControllerDialog', function (): void {
it('validate returns false if controller refresh fails', async function (): Promise<void> { it('validate returns false if controller refresh fails', async function (): Promise<void> {
sinon.stub(ControllerModel.prototype, 'refresh').returns(Promise.reject('Controller refresh failed')); sinon.stub(ControllerModel.prototype, 'refresh').returns(Promise.reject('Controller refresh failed'));
const connectControllerDialog = new ConnectToControllerDialog(undefined!); const connectControllerDialog = new ConnectToControllerDialog(undefined!);
const info = { id: uuid(), url: 'https://127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }; const info: ControllerInfo = { id: uuid(), endpoint: 'https://127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] };
connectControllerDialog.showDialog(info, 'pwd'); connectControllerDialog.showDialog(info, 'pwd');
await connectControllerDialog.isInitialized; await connectControllerDialog.isInitialized;
const validateResult = await connectControllerDialog.validate(); const validateResult = await connectControllerDialog.validate();
@@ -41,36 +41,36 @@ describe('ConnectControllerDialog', function (): void {
it('validate replaces http with https', async function (): Promise<void> { it('validate replaces http with https', async function (): Promise<void> {
await validateConnectControllerDialog( await validateConnectControllerDialog(
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, { id: uuid(), endpoint: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30081'); 'https://127.0.0.1:30081');
}); });
it('validate appends https if missing', async function (): Promise<void> { it('validate appends https if missing', async function (): Promise<void> {
await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, await validateConnectControllerDialog({ id: uuid(), endpoint: '127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30080'); 'https://127.0.0.1:30080');
}); });
it('validate appends default port if missing', async function (): Promise<void> { it('validate appends default port if missing', async function (): Promise<void> {
await validateConnectControllerDialog({ id: uuid(), url: 'https://127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, await validateConnectControllerDialog({ id: uuid(), endpoint: 'https://127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30080'); 'https://127.0.0.1:30080');
}); });
it('validate appends both port and https if missing', async function (): Promise<void> { it('validate appends both port and https if missing', async function (): Promise<void> {
await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, await validateConnectControllerDialog({ id: uuid(), endpoint: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30080'); 'https://127.0.0.1:30080');
}); });
for (const name of ['', undefined]) { for (const name of ['', undefined]) {
it.skip(`validate display name gets set to arc instance name for user chosen name of:${name}`, async function (): Promise<void> { it.skip(`validate display name gets set to arc instance name for user chosen name of:${name}`, async function (): Promise<void> {
await validateConnectControllerDialog( await validateConnectControllerDialog(
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: name!, username: 'sa', rememberPassword: true, resources: [] }, { id: uuid(), endpoint: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: name!, namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30081'); 'https://127.0.0.1:30081');
}); });
} }
it.skip(`validate display name gets set to default data controller name for user chosen name of:'' and instanceName in explicably returned as undefined from the controller endpoint`, async function (): Promise<void> { it.skip(`validate display name gets set to default data controller name for user chosen name of:'' and instanceName in explicably returned as undefined from the controller endpoint`, async function (): Promise<void> {
await validateConnectControllerDialog( await validateConnectControllerDialog(
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: '', username: 'sa', rememberPassword: true, resources: [] }, { id: uuid(), endpoint: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: '', namespace: 'arc-ns', username: 'sa', rememberPassword: true, resources: [] },
'https://127.0.0.1:30081', 'https://127.0.0.1:30081',
undefined); undefined);
}); });
@@ -92,6 +92,6 @@ async function validateConnectControllerDialog(info: ControllerInfo, expectedUrl
const validateResult = await connectControllerDialog.validate(); const validateResult = await connectControllerDialog.validate();
should(validateResult).be.true('Validation should have returned true'); should(validateResult).be.true('Validation should have returned true');
const model = await connectControllerDialog.waitForClose(); const model = await connectControllerDialog.waitForClose();
should(model?.controllerModel.info.url).equal(expectedUrl); should(model?.controllerModel.info.endpoint).equal(expectedUrl);
should(model?.controllerModel.info.name).equal(expectedControllerInfoName); should(model?.controllerModel.info.name).equal(expectedControllerInfoName);
} }

View File

@@ -24,6 +24,20 @@ interface ExtensionGlobalMemento extends vscode.Memento {
setKeysForSync(keys: string[]): void; setKeysForSync(keys: string[]): void;
} }
function getDefaultControllerInfo(): ControllerInfo {
return {
id: uuid(),
endpoint: '127.0.0.1',
kubeConfigFilePath: '/path/to/.kube/config',
kubeClusterContext: 'currentCluster',
username: 'sa',
name: 'my-arc',
namespace: 'arc-ns',
rememberPassword: true,
resources: []
};
}
describe('AzureArcTreeDataProvider tests', function (): void { describe('AzureArcTreeDataProvider tests', function (): void {
let treeDataProvider: AzureArcTreeDataProvider; let treeDataProvider: AzureArcTreeDataProvider;
beforeEach(function (): void { beforeEach(function (): void {
@@ -58,7 +72,7 @@ describe('AzureArcTreeDataProvider tests', function (): void {
treeDataProvider['_loading'] = false; treeDataProvider['_loading'] = false;
let children = await treeDataProvider.getChildren(); let children = await treeDataProvider.getChildren();
should(children.length).equal(0, 'There initially shouldn\'t be any children'); should(children.length).equal(0, 'There initially shouldn\'t be any children');
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }); const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
await treeDataProvider.addOrUpdateController(controllerModel, ''); await treeDataProvider.addOrUpdateController(controllerModel, '');
should(children.length).equal(1, 'Controller node should be added correctly'); should(children.length).equal(1, 'Controller node should be added correctly');
await treeDataProvider.addOrUpdateController(controllerModel, ''); await treeDataProvider.addOrUpdateController(controllerModel, '');
@@ -69,12 +83,12 @@ describe('AzureArcTreeDataProvider tests', function (): void {
treeDataProvider['_loading'] = false; treeDataProvider['_loading'] = false;
let children = await treeDataProvider.getChildren(); let children = await treeDataProvider.getChildren();
should(children.length).equal(0, 'There initially shouldn\'t be any children'); should(children.length).equal(0, 'There initially shouldn\'t be any children');
const originalInfo: ControllerInfo = { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }; const originalInfo: ControllerInfo = getDefaultControllerInfo();
const controllerModel = new ControllerModel(treeDataProvider, originalInfo); const controllerModel = new ControllerModel(treeDataProvider, originalInfo);
await treeDataProvider.addOrUpdateController(controllerModel, ''); await treeDataProvider.addOrUpdateController(controllerModel, '');
should(children.length).equal(1, 'Controller node should be added correctly'); should(children.length).equal(1, 'Controller node should be added correctly');
should((<ControllerTreeNode>children[0]).model.info).deepEqual(originalInfo); should((<ControllerTreeNode>children[0]).model.info).deepEqual(originalInfo);
const newInfo = { id: originalInfo.id, url: '1.1.1.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'new-name', username: 'admin', rememberPassword: false, resources: [] }; const newInfo: ControllerInfo = { id: originalInfo.id, endpoint: '1.1.1.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'new-name', namespace: 'new-namespace', username: 'admin', rememberPassword: false, resources: [] };
const controllerModel2 = new ControllerModel(treeDataProvider, newInfo); const controllerModel2 = new ControllerModel(treeDataProvider, newInfo);
await treeDataProvider.addOrUpdateController(controllerModel2, ''); await treeDataProvider.addOrUpdateController(controllerModel2, '');
should(children.length).equal(1, 'Shouldn\'t add duplicate controller node'); should(children.length).equal(1, 'Shouldn\'t add duplicate controller node');
@@ -109,8 +123,8 @@ describe('AzureArcTreeDataProvider tests', function (): void {
mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi); mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi);
sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object); sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object);
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').resolves([{ name: 'currentCluster', isCurrentContext: true }]); sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').returns([{ name: 'currentCluster', isCurrentContext: true }]);
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, 'mypassword'); const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo(), 'mypassword');
await treeDataProvider.addOrUpdateController(controllerModel, ''); await treeDataProvider.addOrUpdateController(controllerModel, '');
const controllerNode = treeDataProvider.getControllerNode(controllerModel); const controllerNode = treeDataProvider.getControllerNode(controllerModel);
const children = await treeDataProvider.getChildren(controllerNode); const children = await treeDataProvider.getChildren(controllerNode);
@@ -123,8 +137,10 @@ describe('AzureArcTreeDataProvider tests', function (): void {
describe('removeController', function (): void { describe('removeController', function (): void {
it('removing a controller should work as expected', async function (): Promise<void> { it('removing a controller should work as expected', async function (): Promise<void> {
treeDataProvider['_loading'] = false; treeDataProvider['_loading'] = false;
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }); const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
const controllerModel2 = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.2', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'cloudsa', rememberPassword: true, resources: [] }); const info2 = getDefaultControllerInfo();
info2.username = 'cloudsa';
const controllerModel2 = new ControllerModel(treeDataProvider, info2);
await treeDataProvider.addOrUpdateController(controllerModel, ''); await treeDataProvider.addOrUpdateController(controllerModel, '');
await treeDataProvider.addOrUpdateController(controllerModel2, ''); await treeDataProvider.addOrUpdateController(controllerModel2, '');
const children = <ControllerTreeNode[]>(await treeDataProvider.getChildren()); const children = <ControllerTreeNode[]>(await treeDataProvider.getChildren());
@@ -141,20 +157,20 @@ describe('AzureArcTreeDataProvider tests', function (): void {
describe('openResourceDashboard', function (): void { describe('openResourceDashboard', function (): void {
it('Opening dashboard for nonexistent controller node throws', async function (): Promise<void> { it('Opening dashboard for nonexistent controller node throws', async function (): Promise<void> {
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }); const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, ''); const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
await should(openDashboardPromise).be.rejected(); await should(openDashboardPromise).be.rejected();
}); });
it('Opening dashboard for nonexistent resource throws', async function (): Promise<void> { it('Opening dashboard for nonexistent resource throws', async function (): Promise<void> {
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }); const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
await treeDataProvider.addOrUpdateController(controllerModel, ''); await treeDataProvider.addOrUpdateController(controllerModel, '');
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, ''); const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
await should(openDashboardPromise).be.rejected(); await should(openDashboardPromise).be.rejected();
}); });
it('Opening dashboard for existing resource node succeeds', async function (): Promise<void> { it('Opening dashboard for existing resource node succeeds', async function (): Promise<void> {
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }); const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
const miaaModel = new MiaaModel(controllerModel, { name: 'miaa-1', resourceType: ResourceType.sqlManagedInstances }, undefined!, treeDataProvider); const miaaModel = new MiaaModel(controllerModel, { name: 'miaa-1', resourceType: ResourceType.sqlManagedInstances }, undefined!, treeDataProvider);
await treeDataProvider.addOrUpdateController(controllerModel, ''); await treeDataProvider.addOrUpdateController(controllerModel, '');
const controllerNode = treeDataProvider.getControllerNode(controllerModel)!; const controllerNode = treeDataProvider.getControllerNode(controllerModel)!;

View File

@@ -37,7 +37,8 @@ declare module 'arc' {
id: string, id: string,
kubeConfigFilePath: string, kubeConfigFilePath: string,
kubeClusterContext: string kubeClusterContext: string
url: string, endpoint: string | undefined,
namespace: string,
name: string, name: string,
username: string, username: string,
rememberPassword: boolean, rememberPassword: boolean,

View File

@@ -17,6 +17,9 @@ export class RadioOptionsGroup {
private _loadingBuilder: azdata.LoadingComponentBuilder; private _loadingBuilder: azdata.LoadingComponentBuilder;
private _currentRadioOption!: azdata.RadioButtonComponent; private _currentRadioOption!: azdata.RadioButtonComponent;
private _onRadioOptionChanged: vscode.EventEmitter<string | undefined> = new vscode.EventEmitter<string | undefined>();
public onRadioOptionChanged: vscode.Event<string | undefined> = this._onRadioOptionChanged.event;
constructor(private _modelBuilder: azdata.ModelBuilder, private _onNewDisposableCreated: (disposable: vscode.Disposable) => void, private _groupName: string = `RadioOptionsGroup${RadioOptionsGroup.id++}`) { constructor(private _modelBuilder: azdata.ModelBuilder, private _onNewDisposableCreated: (disposable: vscode.Disposable) => void, private _groupName: string = `RadioOptionsGroup${RadioOptionsGroup.id++}`) {
this._divContainer = this._modelBuilder.divContainer().withProperties<azdata.DivContainerProperties>({ clickable: false }).component(); this._divContainer = this._modelBuilder.divContainer().withProperties<azdata.DivContainerProperties>({ clickable: false }).component();
this._loadingBuilder = this._modelBuilder.loadingComponent().withItem(this._divContainer); this._loadingBuilder = this._modelBuilder.loadingComponent().withItem(this._divContainer);
@@ -26,7 +29,7 @@ export class RadioOptionsGroup {
return this._loadingBuilder.component(); return this._loadingBuilder.component();
} }
async load(optionsInfoGetter: () => Promise<RadioOptionsInfo>): Promise<void> { async load(optionsInfoGetter: () => RadioOptionsInfo | Promise<RadioOptionsInfo>): Promise<void> {
this.component().loading = true; this.component().loading = true;
this._divContainer.clearItems(); this._divContainer.clearItems();
try { try {
@@ -51,6 +54,7 @@ export class RadioOptionsGroup {
// it is just better to keep things clean. // it is just better to keep things clean.
this._currentRadioOption.checked = false; this._currentRadioOption.checked = false;
this._currentRadioOption = radioOption; this._currentRadioOption = radioOption;
this._onRadioOptionChanged.fire(this.value);
} }
})); }));
this._divContainer.addItem(radioOption); this._divContainer.addItem(radioOption);

View File

@@ -129,16 +129,12 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
cancellable: false cancellable: false
}, },
async (_progress, _token): Promise<void> => { async (_progress, _token): Promise<void> => {
let session: azdataExt.AzdataSession | undefined = undefined;
try { try {
session = await this._miaaModel.controllerModel.acquireAzdataSession();
await this._azdataApi.azdata.arc.sql.mi.edit( await this._azdataApi.azdata.arc.sql.mi.edit(
this._miaaModel.info.name, this.saveArgs, this._miaaModel.controllerModel.azdataAdditionalEnvVars, session); this._miaaModel.info.name, this.saveArgs, this._miaaModel.controllerModel.azdataAdditionalEnvVars, this._miaaModel.controllerModel.controllerContext);
} catch (err) { } catch (err) {
this.saveButton!.enabled = true; this.saveButton!.enabled = true;
throw err; throw err;
} finally {
session?.dispose();
} }
try { try {
await this._miaaModel.refresh(); await this._miaaModel.refresh();

View File

@@ -244,12 +244,7 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
cancellable: false cancellable: false
}, },
async (_progress, _token) => { async (_progress, _token) => {
const session = await this._controllerModel.acquireAzdataSession(); return await this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name, this._controllerModel.azdataAdditionalEnvVars, this._controllerModel.controllerContext);
try {
return await this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name, this._controllerModel.azdataAdditionalEnvVars, session);
} finally {
session.dispose();
}
} }
); );
await this._controllerModel.refreshTreeNode(); await this._controllerModel.refreshTreeNode();

View File

@@ -179,9 +179,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
cancellable: false cancellable: false
}, },
async (_progress, _token): Promise<void> => { async (_progress, _token): Promise<void> => {
let session: azdataExt.AzdataSession | undefined = undefined;
try { try {
session = await this._postgresModel.controllerModel.acquireAzdataSession();
await this._azdataApi.azdata.arc.postgres.server.edit( await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name, this._postgresModel.info.name,
{ {
@@ -191,10 +189,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
memoryRequest: this.saveArgs.workerMemoryRequest, memoryRequest: this.saveArgs.workerMemoryRequest,
memoryLimit: this.saveArgs.workerMemoryLimit memoryLimit: this.saveArgs.workerMemoryLimit
}, },
this._postgresModel.engineVersion, this._postgresModel.controllerModel.azdataAdditionalEnvVars);
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session
);
/* TODO add second edit call for coordinator configuration /* TODO add second edit call for coordinator configuration
await this._azdataApi.azdata.arc.postgres.server.edit( await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name, this._postgresModel.info.name,
@@ -204,7 +199,6 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
memoryRequest: this.saveArgs.coordinatorMemoryRequest, memoryRequest: this.saveArgs.coordinatorMemoryRequest,
memoryLimit: this.saveArgs.coordinatorMemoryLimit memoryLimit: this.saveArgs.coordinatorMemoryLimit
}, },
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars, this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session session
); );
@@ -214,8 +208,6 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
// the edit wasn't successfully applied // the edit wasn't successfully applied
this.saveButton.enabled = true; this.saveButton.enabled = true;
throw err; throw err;
} finally {
session?.dispose();
} }
try { try {
await this._postgresModel.refresh(); await this._postgresModel.refresh();

View File

@@ -39,8 +39,7 @@ export class PostgresCoordinatorNodeParametersPage extends PostgresParametersPag
/* TODO add correct azdata call for editing coordinator parameters /* TODO add correct azdata call for editing coordinator parameters
await this._azdataApi.azdata.arc.postgres.server.edit( await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name, this._postgresModel.info.name,
{ engineSettings: engineSettings }, { engineSettings: engineSettings.toString() },
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars, this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session); session);
*/ */
@@ -51,7 +50,6 @@ export class PostgresCoordinatorNodeParametersPage extends PostgresParametersPag
await this._azdataApi.azdata.arc.postgres.server.edit( await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name, this._postgresModel.info.name,
{ engineSettings: `''`, replaceEngineSettings: true }, { engineSettings: `''`, replaceEngineSettings: true },
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars, this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session); session);
*/ */
@@ -62,7 +60,6 @@ export class PostgresCoordinatorNodeParametersPage extends PostgresParametersPag
await this._azdataApi.azdata.arc.postgres.server.edit( await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name, this._postgresModel.info.name,
{ engineSettings: parameterName + '=' }, { engineSettings: parameterName + '=' },
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars, this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session); session);
*/ */

View File

@@ -39,7 +39,7 @@ export class PostgresDashboard extends Dashboard {
// TODO Add dashboard once backend is able to be connected for per role server parameter edits. // TODO Add dashboard once backend is able to be connected for per role server parameter edits.
// const coordinatorNodeParametersPage = new PostgresCoordinatorNodeParametersPage(modelView, this._postgresModel); // const coordinatorNodeParametersPage = new PostgresCoordinatorNodeParametersPage(modelView, this._postgresModel);
const workerNodeParametersPage = new PostgresWorkerNodeParametersPage(modelView, this.dashboard, this._postgresModel); const workerNodeParametersPage = new PostgresWorkerNodeParametersPage(modelView, this.dashboard, this._postgresModel);
const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this.dashboard, this._context, this._postgresModel); const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this.dashboard, this._context, this._controllerModel, this._postgresModel);
const supportRequestPage = new PostgresSupportRequestPage(modelView, this.dashboard, this._controllerModel, this._postgresModel); const supportRequestPage = new PostgresSupportRequestPage(modelView, this.dashboard, this._controllerModel, this._postgresModel);
const resourceHealthPage = new PostgresResourceHealthPage(modelView, this.dashboard, this._postgresModel); const resourceHealthPage = new PostgresResourceHealthPage(modelView, this.dashboard, this._postgresModel);

View File

@@ -9,9 +9,10 @@ import * as loc from '../../../localizedConstants';
import { IconPathHelper, cssStyles } from '../../../constants'; import { IconPathHelper, cssStyles } from '../../../constants';
import { DashboardPage } from '../../components/dashboardPage'; import { DashboardPage } from '../../components/dashboardPage';
import { PostgresModel } from '../../../models/postgresModel'; import { PostgresModel } from '../../../models/postgresModel';
import { ControllerModel } from '../../../models/controllerModel';
export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage { export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage {
constructor(protected modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _context: vscode.ExtensionContext, private _postgresModel: PostgresModel) { constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _context: vscode.ExtensionContext, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(modelView, dashboard); super(modelView, dashboard);
} }
@@ -50,9 +51,8 @@ export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage {
this.disposables.push( this.disposables.push(
troubleshootButton.onDidClick(() => { troubleshootButton.onDidClick(() => {
process.env['POSTGRES_SERVER_NAMESPACE'] = this._postgresModel.config?.metadata.namespace; process.env['POSTGRES_SERVER_NAMESPACE'] = this._controllerModel.controllerConfig?.metadata.namespace ?? '';
process.env['POSTGRES_SERVER_NAME'] = this._postgresModel.info.name; process.env['POSTGRES_SERVER_NAME'] = this._postgresModel.info.name;
process.env['POSTGRES_SERVER_VERSION'] = this._postgresModel.engineVersion;
vscode.commands.executeCommand('bookTreeView.openBook', this._context.asAbsolutePath('notebooks/arcDataServices'), true, 'postgres/tsg100-troubleshoot-postgres'); vscode.commands.executeCommand('bookTreeView.openBook', this._context.asAbsolutePath('notebooks/arcDataServices'), true, 'postgres/tsg100-troubleshoot-postgres');
})); }));

View File

@@ -217,21 +217,13 @@ export class PostgresOverviewPage extends DashboardPage {
try { try {
const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : ''); const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : '');
if (password) { if (password) {
const session = await this._postgresModel.controllerModel.acquireAzdataSession(); await this._azdataApi.azdata.arc.postgres.server.edit(
try { this._postgresModel.info.name,
await this._azdataApi.azdata.arc.postgres.server.edit( {
this._postgresModel.info.name, adminPassword: true,
{ noWait: true
adminPassword: true, },
noWait: true Object.assign({ 'AZDATA_PASSWORD': password }, this._controllerModel.azdataAdditionalEnvVars));
},
this._postgresModel.engineVersion,
Object.assign({ 'AZDATA_PASSWORD': password }, this._controllerModel.azdataAdditionalEnvVars),
session
);
} finally {
session.dispose();
}
vscode.window.showInformationMessage(loc.passwordReset); vscode.window.showInformationMessage(loc.passwordReset);
} }
} catch (error) { } catch (error) {
@@ -259,13 +251,7 @@ export class PostgresOverviewPage extends DashboardPage {
cancellable: false cancellable: false
}, },
async (_progress, _token) => { async (_progress, _token) => {
const session = await this._postgresModel.controllerModel.acquireAzdataSession(); return await this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name, this._controllerModel.azdataAdditionalEnvVars, this._controllerModel.controllerContext);
try {
return await this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name, this._controllerModel.azdataAdditionalEnvVars, session);
} finally {
session.dispose();
}
} }
); );
await this._controllerModel.refreshTreeNode(); await this._controllerModel.refreshTreeNode();

View File

@@ -152,12 +152,7 @@ export abstract class PostgresParametersPage extends DashboardPage {
this.parameterUpdates.forEach((value, key) => { this.parameterUpdates.forEach((value, key) => {
engineSettings.push(`${key}="${value}"`); engineSettings.push(`${key}="${value}"`);
}); });
const session = await this._postgresModel.controllerModel.acquireAzdataSession(); await this.saveParameterEdits(engineSettings.toString());
try {
await this.saveParameterEdits(engineSettings.toString(), session);
} finally {
session.dispose();
}
} catch (err) { } catch (err) {
// If an error occurs while editing the instance then re-enable the save button since // If an error occurs while editing the instance then re-enable the save button since
// the edit wasn't successfully applied // the edit wasn't successfully applied
@@ -230,12 +225,7 @@ export abstract class PostgresParametersPage extends DashboardPage {
}, },
async (_progress, _token): Promise<void> => { async (_progress, _token): Promise<void> => {
try { try {
const session = await this._postgresModel.controllerModel.acquireAzdataSession(); await this.resetAllParameters();
try {
await this.resetAllParameters(session);
} finally {
session.dispose();
}
} catch (err) { } catch (err) {
// If an error occurs while resetting the instance then re-enable the reset button since // If an error occurs while resetting the instance then re-enable the reset button since
// the edit wasn't successfully applied // the edit wasn't successfully applied
@@ -423,12 +413,7 @@ export abstract class PostgresParametersPage extends DashboardPage {
cancellable: false cancellable: false
}, },
async (_progress, _token): Promise<void> => { async (_progress, _token): Promise<void> => {
const session = await this._postgresModel.controllerModel.acquireAzdataSession(); await this.resetParameter(engineSetting.parameterName!);
try {
await this.resetParameter(engineSetting.parameterName!, session);
} finally {
session.dispose();
}
try { try {
await this._postgresModel.refresh(); await this._postgresModel.refresh();
} catch (error) { } catch (error) {
@@ -633,9 +618,9 @@ export abstract class PostgresParametersPage extends DashboardPage {
} }
} }
protected abstract saveParameterEdits(engineSettings: string, session: azdataExt.AzdataSession): Promise<void>; protected abstract saveParameterEdits(engineSettings: string): Promise<void>;
protected abstract resetAllParameters(session: azdataExt.AzdataSession): Promise<void>; protected abstract resetAllParameters(): Promise<void>;
protected abstract resetParameter(parameterName: string, session: azdataExt.AzdataSession): Promise<void>; protected abstract resetParameter(parameterName: string): Promise<void>;
} }

View File

@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import * as loc from '../../../localizedConstants'; import * as loc from '../../../localizedConstants';
import { IconPathHelper } from '../../../constants'; import { IconPathHelper } from '../../../constants';
import { PostgresParametersPage } from './postgresParameters'; import { PostgresParametersPage } from './postgresParameters';
@@ -35,34 +34,32 @@ export class PostgresWorkerNodeParametersPage extends PostgresParametersPage {
return loc.nodeParametersDescription; return loc.nodeParametersDescription;
} }
protected get engineSettings(): EngineSettingsModel[] { protected get engineSettings(): EngineSettingsModel[] {
return this._postgresModel.workerNodesEngineSettings; return this._postgresModel.workerNodesEngineSettings;
} }
protected async saveParameterEdits(engineSettings: string, session: azdataExt.AzdataSession): Promise<void> { protected async saveParameterEdits(engineSettings: string): Promise<void> {
await this._azdataApi.azdata.arc.postgres.server.edit( await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name, this._postgresModel.info.name,
{ engineSettings: engineSettings }, { engineSettings: engineSettings },
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars, this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session); this._postgresModel.controllerModel.controllerContext);
} }
protected async resetAllParameters(session: azdataExt.AzdataSession): Promise<void> { protected async resetAllParameters(): Promise<void> {
await this._azdataApi.azdata.arc.postgres.server.edit( await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name, this._postgresModel.info.name,
{ engineSettings: `''`, replaceEngineSettings: true }, { engineSettings: `''`, replaceEngineSettings: true },
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars, this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session); this._postgresModel.controllerModel.controllerContext);
} }
protected async resetParameter(parameterName: string, session: azdataExt.AzdataSession): Promise<void> { protected async resetParameter(parameterName: string): Promise<void> {
await this._azdataApi.azdata.arc.postgres.server.edit( await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name, this._postgresModel.info.name,
{ engineSettings: parameterName + '=' }, { engineSettings: parameterName + '=' },
this._postgresModel.engineVersion,
this._postgresModel.controllerModel.azdataAdditionalEnvVars, this._postgresModel.controllerModel.azdataAdditionalEnvVars,
session); this._postgresModel.controllerModel.controllerContext);
} }
} }

View File

@@ -15,7 +15,7 @@ import { InitializingComponent } from '../components/initializingComponent';
import { AzureArcTreeDataProvider } from '../tree/azureArcTreeDataProvider'; import { AzureArcTreeDataProvider } from '../tree/azureArcTreeDataProvider';
import { getErrorMessage } from '../../common/utils'; import { getErrorMessage } from '../../common/utils';
import { RadioOptionsGroup } from '../components/radioOptionsGroup'; import { RadioOptionsGroup } from '../components/radioOptionsGroup';
import { getCurrentClusterContext, getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../../common/kubeUtils'; import { getCurrentClusterContext, getDefaultKubeConfigPath, getKubeConfigClusterContexts, KubeClusterContext } from '../../common/kubeUtils';
import { FilePicker } from '../components/filePicker'; import { FilePicker } from '../components/filePicker';
export type ConnectToControllerDialogModel = { controllerModel: ControllerModel, password: string }; export type ConnectToControllerDialogModel = { controllerModel: ControllerModel, password: string };
@@ -25,24 +25,34 @@ abstract class ControllerDialogBase extends InitializingComponent {
protected modelBuilder!: azdata.ModelBuilder; protected modelBuilder!: azdata.ModelBuilder;
protected dialog: azdata.window.Dialog; protected dialog: azdata.window.Dialog;
protected urlInputBox!: azdata.InputBoxComponent; protected namespaceInputBox!: azdata.InputBoxComponent;
protected kubeConfigInputBox!: FilePicker; protected kubeConfigInputBox!: FilePicker;
protected clusterContextRadioGroup!: RadioOptionsGroup; protected clusterContextRadioGroup!: RadioOptionsGroup;
protected nameInputBox!: azdata.InputBoxComponent; protected nameInputBox!: azdata.InputBoxComponent;
protected usernameInputBox!: azdata.InputBoxComponent; protected usernameInputBox!: azdata.InputBoxComponent;
protected passwordInputBox!: azdata.InputBoxComponent; protected passwordInputBox!: azdata.InputBoxComponent;
protected urlInputBox!: azdata.InputBoxComponent;
private _kubeClusters: KubeClusterContext[] = [];
protected dispose(): void { protected dispose(): void {
this._toDispose.forEach(disposable => disposable.dispose()); this._toDispose.forEach(disposable => disposable.dispose());
this._toDispose.length = 0; // clear the _toDispose array this._toDispose.length = 0;
} }
protected getComponents(): (azdata.FormComponent<azdata.Component> & { layout?: azdata.FormItemLayout | undefined; })[] { protected getComponents(): (azdata.FormComponent<azdata.Component> & { layout?: azdata.FormItemLayout | undefined; })[] {
return [ return [
{
component: this.namespaceInputBox,
title: loc.namespace,
required: true
},
{ {
component: this.urlInputBox, component: this.urlInputBox,
title: loc.controllerUrl, title: loc.controllerUrl,
required: true layout: {
info: loc.controllerUrlDescription
}
}, { }, {
component: this.kubeConfigInputBox.component(), component: this.kubeConfigInputBox.component(),
title: loc.controllerKubeConfig, title: loc.controllerKubeConfig,
@@ -54,14 +64,17 @@ abstract class ControllerDialogBase extends InitializingComponent {
}, { }, {
component: this.nameInputBox, component: this.nameInputBox,
title: loc.controllerName, title: loc.controllerName,
required: false required: false,
layout: {
info: loc.controllerNameDescription
}
}, { }, {
component: this.usernameInputBox, component: this.usernameInputBox,
title: loc.username, title: loc.controllerUsername,
required: true required: true
}, { }, {
component: this.passwordInputBox, component: this.passwordInputBox,
title: loc.password, title: loc.controllerPassword,
required: true required: true
} }
]; ];
@@ -71,11 +84,14 @@ abstract class ControllerDialogBase extends InitializingComponent {
protected readonlyFields(): azdata.Component[] { return []; } protected readonlyFields(): azdata.Component[] { return []; }
protected initializeFields(controllerInfo: ControllerInfo | undefined, password: string | undefined) { protected initializeFields(controllerInfo: ControllerInfo | undefined, password: string | undefined) {
this.namespaceInputBox = this.modelBuilder.inputBox()
.withProps({
value: controllerInfo?.namespace,
}).component();
this.urlInputBox = this.modelBuilder.inputBox() this.urlInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({ .withProps({
value: controllerInfo?.url, value: controllerInfo?.endpoint,
// If we have a model then we're editing an existing connection so don't let them modify the URL placeHolder: loc.controllerUrlPlaceholder,
readOnly: !!controllerInfo
}).component(); }).component();
this.kubeConfigInputBox = new FilePicker( this.kubeConfigInputBox = new FilePicker(
this.modelBuilder, this.modelBuilder,
@@ -83,22 +99,23 @@ abstract class ControllerDialogBase extends InitializingComponent {
(disposable) => this._toDispose.push(disposable) (disposable) => this._toDispose.push(disposable)
); );
this.modelBuilder.inputBox() this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({ .withProps({
value: controllerInfo?.kubeConfigFilePath || getDefaultKubeConfigPath() value: controllerInfo?.kubeConfigFilePath || getDefaultKubeConfigPath()
}).component(); }).component();
this.clusterContextRadioGroup = new RadioOptionsGroup(this.modelBuilder, (disposable) => this._toDispose.push(disposable)); this.clusterContextRadioGroup = new RadioOptionsGroup(this.modelBuilder, (disposable) => this._toDispose.push(disposable));
this.loadRadioGroup(controllerInfo?.kubeClusterContext); this.loadRadioGroup(controllerInfo?.kubeClusterContext);
this._toDispose.push(this.clusterContextRadioGroup.onRadioOptionChanged(newContext => this.updateNamespace(newContext)));
this._toDispose.push(this.kubeConfigInputBox.onTextChanged(() => this.loadRadioGroup(controllerInfo?.kubeClusterContext))); this._toDispose.push(this.kubeConfigInputBox.onTextChanged(() => this.loadRadioGroup(controllerInfo?.kubeClusterContext)));
this.nameInputBox = this.modelBuilder.inputBox() this.nameInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({ .withProps({
value: controllerInfo?.name value: controllerInfo?.name
}).component(); }).component();
this.usernameInputBox = this.modelBuilder.inputBox() this.usernameInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({ .withProps({
value: controllerInfo?.username value: controllerInfo?.username
}).component(); }).component();
this.passwordInputBox = this.modelBuilder.inputBox() this.passwordInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({ .withProps({
inputType: 'password', inputType: 'password',
value: password value: password
}).component(); }).component();
@@ -114,15 +131,22 @@ abstract class ControllerDialogBase extends InitializingComponent {
} }
private loadRadioGroup(previousClusterContext?: string): void { private loadRadioGroup(previousClusterContext?: string): void {
this.clusterContextRadioGroup.load(async () => { this.clusterContextRadioGroup.load(() => {
const clusters = await getKubeConfigClusterContexts(this.kubeConfigInputBox.value!); this._kubeClusters = getKubeConfigClusterContexts(this.kubeConfigInputBox.value!);
const currentClusterContext = getCurrentClusterContext(this._kubeClusters, previousClusterContext, false);
this.namespaceInputBox.value = currentClusterContext.namespace || this.namespaceInputBox.value;
return { return {
values: clusters.map(c => c.name), values: this._kubeClusters.map(c => c.name),
defaultValue: getCurrentClusterContext(clusters, previousClusterContext, false), defaultValue: currentClusterContext.name
}; };
}); });
} }
private updateNamespace(currentContextName: string | undefined): void {
const currentContext = this._kubeClusters.find(cluster => cluster.name === currentContextName);
this.namespaceInputBox.value = currentContext?.namespace;
}
public showDialog(controllerInfo?: ControllerInfo, password: string | undefined = undefined): azdata.window.Dialog { public showDialog(controllerInfo?: ControllerInfo, password: string | undefined = undefined): azdata.window.Dialog {
this.id = controllerInfo?.id ?? uuid(); this.id = controllerInfo?.id ?? uuid();
this.resources = controllerInfo?.resources ?? []; this.resources = controllerInfo?.resources ?? [];
@@ -168,7 +192,8 @@ abstract class ControllerDialogBase extends InitializingComponent {
protected getControllerInfo(url: string, rememberPassword: boolean = false): ControllerInfo { protected getControllerInfo(url: string, rememberPassword: boolean = false): ControllerInfo {
return { return {
id: this.id, id: this.id,
url: url, endpoint: url || undefined,
namespace: this.namespaceInputBox.value!,
kubeConfigFilePath: this.kubeConfigInputBox.value!, kubeConfigFilePath: this.kubeConfigInputBox.value!,
kubeClusterContext: this.clusterContextRadioGroup.value!, kubeClusterContext: this.clusterContextRadioGroup.value!,
name: this.nameInputBox.value ?? '', name: this.nameInputBox.value ?? '',
@@ -183,7 +208,7 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
protected rememberPwCheckBox!: azdata.CheckBoxComponent; protected rememberPwCheckBox!: azdata.CheckBoxComponent;
protected fieldToFocusOn() { protected fieldToFocusOn() {
return this.urlInputBox; return this.namespaceInputBox;
} }
protected getComponents() { protected getComponents() {
@@ -209,22 +234,25 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
} }
public async validate(): Promise<boolean> { public async validate(): Promise<boolean> {
if (!this.urlInputBox.value || !this.usernameInputBox.value || !this.passwordInputBox.value) { if (!this.namespaceInputBox.value || !this.usernameInputBox.value || !this.passwordInputBox.value) {
return false; return false;
} }
let url = this.urlInputBox.value; let url = this.urlInputBox.value || '';
// Only support https connections if (url) {
if (url.toLowerCase().startsWith('http://')) { // Only support https connections
url = url.replace('http', 'https'); if (url.toLowerCase().startsWith('http://')) {
} url = url.replace('http', 'https');
// Append https if they didn't type it in }
if (!url.toLowerCase().startsWith('https://')) { // Append https if they didn't type it in
url = `https://${url}`; if (!url.toLowerCase().startsWith('https://')) {
} url = `https://${url}`;
// Append default port if one wasn't specified }
if (!/.*:\d*$/.test(url)) { // Append default port if one wasn't specified
url = `${url}:30080`; if (!/.*:\d*$/.test(url)) {
url = `${url}:30080`;
}
} }
const controllerInfo: ControllerInfo = this.getControllerInfo(url, !!this.rememberPwCheckBox.checked); const controllerInfo: ControllerInfo = this.getControllerInfo(url, !!this.rememberPwCheckBox.checked);
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value); const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
try { try {
@@ -234,7 +262,7 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
controllerModel.info.name = controllerModel.info.name || controllerModel.controllerConfig?.metadata.name || loc.defaultControllerName; controllerModel.info.name = controllerModel.info.name || controllerModel.controllerConfig?.metadata.name || loc.defaultControllerName;
} catch (err) { } catch (err) {
this.dialog.message = { this.dialog.message = {
text: loc.connectToControllerFailed(this.urlInputBox.value, err), text: loc.connectToControllerFailed(this.namespaceInputBox.value, err),
level: azdata.window.MessageLevel.Error level: azdata.window.MessageLevel.Error
}; };
return false; return false;
@@ -267,11 +295,16 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
if (!this.passwordInputBox.value) { if (!this.passwordInputBox.value) {
return false; return false;
} }
const controllerInfo: ControllerInfo = this.getControllerInfo(this.urlInputBox.value!, false);
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
const azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports; const azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
try { try {
await azdataApi.azdata.login( await azdataApi.azdata.login(
this.urlInputBox.value!, {
this.usernameInputBox.value!, endpoint: controllerInfo.endpoint,
namespace: controllerInfo.namespace
},
controllerInfo.username,
this.passwordInputBox.value, this.passwordInputBox.value,
{ {
'KUBECONFIG': this.kubeConfigInputBox.value!, 'KUBECONFIG': this.kubeConfigInputBox.value!,
@@ -293,8 +326,6 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
return false; return false;
} }
} }
const controllerInfo: ControllerInfo = this.getControllerInfo(this.urlInputBox.value!, false);
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
this.completionPromise.resolve({ controllerModel: controllerModel, password: this.passwordInputBox.value }); this.completionPromise.resolve({ controllerModel: controllerModel, password: this.passwordInputBox.value });
return true; return true;
} }

View File

@@ -10,7 +10,7 @@ import { ControllerModel } from '../../models/controllerModel';
import { ControllerTreeNode } from './controllerTreeNode'; import { ControllerTreeNode } from './controllerTreeNode';
import { TreeNode } from './treeNode'; import { TreeNode } from './treeNode';
const mementoToken = 'arcDataControllers'; const mementoToken = 'arcDataControllers.v2';
/** /**
* The TreeDataProvider for the Azure Arc view, which displays a list of registered * The TreeDataProvider for the Azure Arc view, which displays a list of registered

View File

@@ -44,7 +44,7 @@ export class ControllerTreeNode extends TreeNode {
} catch (err) { } catch (err) {
vscode.window.showErrorMessage(loc.errorConnectingToController(err)); vscode.window.showErrorMessage(loc.errorConnectingToController(err));
try { try {
await this.model.refresh(false, true); await this.model.refresh(false);
this.updateChildren(this.model.registrations); this.updateChildren(this.model.registrations);
} catch (err) { } catch (err) {
if (!(err instanceof UserCancelledError)) { if (!(err instanceof UserCancelledError)) {

View File

@@ -2,7 +2,7 @@
"name": "azdata", "name": "azdata",
"displayName": "%azdata.displayName%", "displayName": "%azdata.displayName%",
"description": "%azdata.description%", "description": "%azdata.description%",
"version": "0.6.0", "version": "0.6.2",
"publisher": "Microsoft", "publisher": "Microsoft",
"preview": true, "preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",

View File

@@ -5,13 +5,26 @@
import * as azdataExt from 'azdata-ext'; import * as azdataExt from 'azdata-ext';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { IAzdataTool, isEulaAccepted, promptForEula } from './azdata'; import { IAzdataTool, isEulaAccepted, MIN_AZDATA_VERSION, promptForEula } from './azdata';
import Logger from './common/logger'; import Logger from './common/logger';
import { NoAzdataError } from './common/utils'; import { NoAzdataError } from './common/utils';
import * as constants from './constants'; import * as constants from './constants';
import * as loc from './localizedConstants'; import * as loc from './localizedConstants';
import { AzdataToolService } from './services/azdataToolService'; import { AzdataToolService } from './services/azdataToolService';
/**
* Validates that :
* - Azdata is installed
* - The Azdata version is >= the minimum required version
* - The Azdata CLI has been accepted
* @param azdata The azdata tool to check
* @param eulaAccepted Whether the Azdata CLI EULA has been accepted
*/
async function validateAzdata(azdata: IAzdataTool | undefined, eulaAccepted: boolean): Promise<void> {
throwIfNoAzdataOrEulaNotAccepted(azdata, eulaAccepted);
await throwIfRequiredVersionMissing(azdata);
}
export function throwIfNoAzdataOrEulaNotAccepted(azdata: IAzdataTool | undefined, eulaAccepted: boolean): asserts azdata { export function throwIfNoAzdataOrEulaNotAccepted(azdata: IAzdataTool | undefined, eulaAccepted: boolean): asserts azdata {
throwIfNoAzdata(azdata); throwIfNoAzdata(azdata);
if (!eulaAccepted) { if (!eulaAccepted) {
@@ -20,6 +33,13 @@ export function throwIfNoAzdataOrEulaNotAccepted(azdata: IAzdataTool | undefined
} }
} }
export async function throwIfRequiredVersionMissing(azdata: IAzdataTool): Promise<void> {
const currentVersion = await azdata.getSemVersion();
if (currentVersion.compare(MIN_AZDATA_VERSION) < 0) {
throw new Error(loc.missingRequiredVersion(MIN_AZDATA_VERSION.raw));
}
}
export function throwIfNoAzdata(localAzdata: IAzdataTool | undefined): asserts localAzdata { export function throwIfNoAzdata(localAzdata: IAzdataTool | undefined): asserts localAzdata {
if (!localAzdata) { if (!localAzdata) {
Logger.log(loc.noAzdata); Logger.log(loc.noAzdata);
@@ -55,47 +75,47 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
profileName?: string, profileName?: string,
storageClass?: string, storageClass?: string,
additionalEnvVars?: azdataExt.AdditionalEnvVars, additionalEnvVars?: azdataExt.AdditionalEnvVars,
session?: azdataExt.AzdataSession) => { azdataContext?: string) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass, additionalEnvVars, session); return azdataToolService.localAzdata!.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass, additionalEnvVars, azdataContext);
}, },
endpoint: { endpoint: {
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => { list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.dc.endpoint.list(additionalEnvVars, session); return azdataToolService.localAzdata!.arc.dc.endpoint.list(additionalEnvVars, azdataContext);
} }
}, },
config: { config: {
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => { list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.dc.config.list(additionalEnvVars, session); return azdataToolService.localAzdata!.arc.dc.config.list(additionalEnvVars, azdataContext);
}, },
show: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => { show: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.dc.config.show(additionalEnvVars, session); return azdataToolService.localAzdata!.arc.dc.config.show(additionalEnvVars, azdataContext);
} }
} }
}, },
postgres: { postgres: {
server: { server: {
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => { delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.delete(name, additionalEnvVars, session); return azdataToolService.localAzdata!.arc.postgres.server.delete(name, additionalEnvVars, azdataContext);
}, },
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => { list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.list(additionalEnvVars, session); return azdataToolService.localAzdata!.arc.postgres.server.list(additionalEnvVars, azdataContext);
}, },
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => { show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.show(name, additionalEnvVars, session); return azdataToolService.localAzdata!.arc.postgres.server.show(name, additionalEnvVars, azdataContext);
}, },
edit: async ( edit: async (
name: string, name: string,
@@ -112,31 +132,30 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
replaceEngineSettings?: boolean; replaceEngineSettings?: boolean;
workers?: number; workers?: number;
}, },
engineVersion?: string,
additionalEnvVars?: azdataExt.AdditionalEnvVars, additionalEnvVars?: azdataExt.AdditionalEnvVars,
session?: azdataExt.AzdataSession) => { azdataContext?: string) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.edit(name, args, engineVersion, additionalEnvVars, session); return azdataToolService.localAzdata!.arc.postgres.server.edit(name, args, additionalEnvVars, azdataContext);
} }
} }
}, },
sql: { sql: {
mi: { mi: {
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => { delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.delete(name, additionalEnvVars, session); return azdataToolService.localAzdata!.arc.sql.mi.delete(name, additionalEnvVars, azdataContext);
}, },
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => { list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.list(additionalEnvVars, session); return azdataToolService.localAzdata!.arc.sql.mi.list(additionalEnvVars, azdataContext);
}, },
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => { show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.show(name, additionalEnvVars, session); return azdataToolService.localAzdata!.arc.sql.mi.show(name, additionalEnvVars, azdataContext);
}, },
edit: async ( edit: async (
name: string, name: string,
@@ -148,11 +167,11 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
noWait?: boolean; noWait?: boolean;
}, },
additionalEnvVars?: azdataExt.AdditionalEnvVars, additionalEnvVars?: azdataExt.AdditionalEnvVars,
session?: azdataExt.AzdataSession azdataContext?: string
) => { ) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.edit(name, args, additionalEnvVars, session); return azdataToolService.localAzdata!.arc.sql.mi.edit(name, args, additionalEnvVars, azdataContext);
} }
} }
} }
@@ -162,13 +181,9 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
throwIfNoAzdata(azdataToolService.localAzdata); throwIfNoAzdata(azdataToolService.localAzdata);
return azdataToolService.localAzdata.getPath(); return azdataToolService.localAzdata.getPath();
}, },
login: async (endpoint: string, username: string, password: string, additionalEnvVars?: azdataExt.AdditionalEnvVars) => { login: async (endpointOrNamespace: azdataExt.EndpointOrNamespace, username: string, password: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.login(endpoint, username, password, additionalEnvVars); return azdataToolService.localAzdata!.login(endpointOrNamespace, username, password, additionalEnvVars, azdataContext);
},
acquireSession: async (endpoint: string, username: string, password: string, additionEnvVars?: azdataExt.AdditionalEnvVars) => {
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata?.acquireSession(endpoint, username, password, additionEnvVars);
}, },
getSemVersion: async () => { getSemVersion: async () => {
await localAzdataDiscovered; await localAzdataDiscovered;

View File

@@ -13,11 +13,15 @@ import { getPlatformDownloadLink, getPlatformReleaseVersion } from './azdataRele
import { executeCommand, executeSudoCommand, ExitCodeError, ProcessOutput } from './common/childProcess'; import { executeCommand, executeSudoCommand, ExitCodeError, ProcessOutput } from './common/childProcess';
import { HttpClient } from './common/httpClient'; import { HttpClient } from './common/httpClient';
import Logger from './common/logger'; import Logger from './common/logger';
import { Deferred } from './common/promise';
import { getErrorMessage, NoAzdataError, searchForCmd } from './common/utils'; import { getErrorMessage, NoAzdataError, searchForCmd } from './common/utils';
import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataInstallKey, azdataUpdateKey, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants'; import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataInstallKey, azdataUpdateKey, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants';
import * as loc from './localizedConstants'; import * as loc from './localizedConstants';
/**
* The minimum required azdata CLI version for this extension to function properly
*/
export const MIN_AZDATA_VERSION = new SemVer('20.3.2');
export const enum AzdataDeployOption { export const enum AzdataDeployOption {
dontPrompt = 'dontPrompt', dontPrompt = 'dontPrompt',
prompt = 'prompt' prompt = 'prompt'
@@ -32,20 +36,7 @@ export interface IAzdataTool extends azdataExt.IAzdataApi {
* @param args The args to pass to azdata * @param args The args to pass to azdata
* @param parseResult A function used to parse out the raw result into the desired shape * @param parseResult A function used to parse out the raw result into the desired shape
*/ */
executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<R>> executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<R>>
}
class AzdataSession implements azdataExt.AzdataSession {
private _session = new Deferred<void>();
public sessionEnded(): Promise<void> {
return this._session.promise;
}
public dispose(): void {
this._session.resolve();
}
} }
/** /**
@@ -54,9 +45,6 @@ class AzdataSession implements azdataExt.AzdataSession {
export class AzdataTool implements azdataExt.IAzdataApi { export class AzdataTool implements azdataExt.IAzdataApi {
private _semVersion: SemVer; private _semVersion: SemVer;
private _currentSession: azdataExt.AzdataSession | undefined = undefined;
private _currentlyExecutingCommands: Deferred<void>[] = [];
private _queuedCommands: { deferred: Deferred<void>, session?: azdataExt.AzdataSession }[] = [];
constructor(private _path: string, version: string) { constructor(private _path: string, version: string) {
this._semVersion = new SemVer(version); this._semVersion = new SemVer(version);
@@ -90,7 +78,7 @@ export class AzdataTool implements azdataExt.IAzdataApi {
profileName?: string, profileName?: string,
storageClass?: string, storageClass?: string,
additionalEnvVars?: azdataExt.AdditionalEnvVars, additionalEnvVars?: azdataExt.AdditionalEnvVars,
session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => { azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
const args = ['arc', 'dc', 'create', const args = ['arc', 'dc', 'create',
'--namespace', namespace, '--namespace', namespace,
'--name', name, '--name', name,
@@ -104,32 +92,32 @@ export class AzdataTool implements azdataExt.IAzdataApi {
if (storageClass) { if (storageClass) {
args.push('--storage-class', storageClass); args.push('--storage-class', storageClass);
} }
return this.executeCommand<void>(args, additionalEnvVars, session); return this.executeCommand<void>(args, additionalEnvVars, azdataContext);
}, },
endpoint: { endpoint: {
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> => { list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> => {
return this.executeCommand<azdataExt.DcEndpointListResult[]>(['arc', 'dc', 'endpoint', 'list'], additionalEnvVars, session); return this.executeCommand<azdataExt.DcEndpointListResult[]>(['arc', 'dc', 'endpoint', 'list'], additionalEnvVars, azdataContext);
} }
}, },
config: { config: {
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> => { list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> => {
return this.executeCommand<azdataExt.DcConfigListResult[]>(['arc', 'dc', 'config', 'list'], additionalEnvVars, session); return this.executeCommand<azdataExt.DcConfigListResult[]>(['arc', 'dc', 'config', 'list'], additionalEnvVars, azdataContext);
}, },
show: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> => { show: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> => {
return this.executeCommand<azdataExt.DcConfigShowResult>(['arc', 'dc', 'config', 'show'], additionalEnvVars, session); return this.executeCommand<azdataExt.DcConfigShowResult>(['arc', 'dc', 'config', 'show'], additionalEnvVars, azdataContext);
} }
} }
}, },
postgres: { postgres: {
server: { server: {
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => { delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
return this.executeCommand<void>(['arc', 'postgres', 'server', 'delete', '-n', name, '--force'], additionalEnvVars, session); return this.executeCommand<void>(['arc', 'postgres', 'server', 'delete', '-n', name, '--force'], additionalEnvVars, azdataContext);
}, },
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> => { list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> => {
return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list'], additionalEnvVars, session); return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list'], additionalEnvVars, azdataContext);
}, },
show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> => { show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> => {
return this.executeCommand<azdataExt.PostgresServerShowResult>(['arc', 'postgres', 'server', 'show', '-n', name], additionalEnvVars, session); return this.executeCommand<azdataExt.PostgresServerShowResult>(['arc', 'postgres', 'server', 'show', '-n', name], additionalEnvVars, azdataContext);
}, },
edit: ( edit: (
name: string, name: string,
@@ -146,9 +134,8 @@ export class AzdataTool implements azdataExt.IAzdataApi {
replaceEngineSettings?: boolean, replaceEngineSettings?: boolean,
workers?: number workers?: number
}, },
engineVersion?: string,
additionalEnvVars?: azdataExt.AdditionalEnvVars, additionalEnvVars?: azdataExt.AdditionalEnvVars,
session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => { azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name]; const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name];
if (args.adminPassword) { argsArray.push('--admin-password'); } if (args.adminPassword) { argsArray.push('--admin-password'); }
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); } if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
@@ -161,21 +148,20 @@ export class AzdataTool implements azdataExt.IAzdataApi {
if (args.port) { argsArray.push('--port', args.port.toString()); } if (args.port) { argsArray.push('--port', args.port.toString()); }
if (args.replaceEngineSettings) { argsArray.push('--replace-engine-settings'); } if (args.replaceEngineSettings) { argsArray.push('--replace-engine-settings'); }
if (args.workers) { argsArray.push('--workers', args.workers.toString()); } if (args.workers) { argsArray.push('--workers', args.workers.toString()); }
if (engineVersion) { argsArray.push('--engine-version', engineVersion); } return this.executeCommand<void>(argsArray, additionalEnvVars, azdataContext);
return this.executeCommand<void>(argsArray, additionalEnvVars, session);
} }
} }
}, },
sql: { sql: {
mi: { mi: {
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => { delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
return this.executeCommand<void>(['arc', 'sql', 'mi', 'delete', '-n', name], additionalEnvVars, session); return this.executeCommand<void>(['arc', 'sql', 'mi', 'delete', '-n', name], additionalEnvVars, azdataContext);
}, },
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> => { list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> => {
return this.executeCommand<azdataExt.SqlMiListResult[]>(['arc', 'sql', 'mi', 'list'], additionalEnvVars, session); return this.executeCommand<azdataExt.SqlMiListResult[]>(['arc', 'sql', 'mi', 'list'], additionalEnvVars, azdataContext);
}, },
show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> => { show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> => {
return this.executeCommand<azdataExt.SqlMiShowResult>(['arc', 'sql', 'mi', 'show', '-n', name], additionalEnvVars, session); return this.executeCommand<azdataExt.SqlMiShowResult>(['arc', 'sql', 'mi', 'show', '-n', name], additionalEnvVars, azdataContext);
}, },
edit: ( edit: (
name: string, name: string,
@@ -186,8 +172,7 @@ export class AzdataTool implements azdataExt.IAzdataApi {
memoryRequest?: string, memoryRequest?: string,
noWait?: boolean, noWait?: boolean,
}, },
additionalEnvVars?: azdataExt.AdditionalEnvVars, additionalEnvVars?: azdataExt.AdditionalEnvVars
session?: azdataExt.AzdataSession
): Promise<azdataExt.AzdataOutput<void>> => { ): Promise<azdataExt.AzdataOutput<void>> => {
const argsArray = ['arc', 'sql', 'mi', 'edit', '-n', name]; const argsArray = ['arc', 'sql', 'mi', 'edit', '-n', name];
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); } if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
@@ -195,59 +180,22 @@ export class AzdataTool implements azdataExt.IAzdataApi {
if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); } if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); }
if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); } if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
if (args.noWait) { argsArray.push('--no-wait'); } if (args.noWait) { argsArray.push('--no-wait'); }
return this.executeCommand<void>(argsArray, additionalEnvVars, session); return this.executeCommand<void>(argsArray, additionalEnvVars);
} }
} }
} }
}; };
public async login(endpoint: string, username: string, password: string, additionalEnvVars: azdataExt.AdditionalEnvVars = {}): Promise<azdataExt.AzdataOutput<void>> { public async login(endpointOrNamespace: azdataExt.EndpointOrNamespace, username: string, password: string, additionalEnvVars: azdataExt.AdditionalEnvVars = {}, azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> {
// Since login changes the context we want to wait until all currently executing commands are finished before this is executed const args = ['login', '-u', username];
while (this._currentlyExecutingCommands.length > 0) { if (endpointOrNamespace.endpoint) {
await this._currentlyExecutingCommands[0]; args.push('-e', endpointOrNamespace.endpoint);
} } else if (endpointOrNamespace.namespace) {
// Logins need to be done outside the session aware logic so call impl directly args.push('--namespace', endpointOrNamespace.namespace);
return this.executeCommandImpl<void>(['login', '-e', endpoint, '-u', username], Object.assign({}, additionalEnvVars, { 'AZDATA_PASSWORD': password }));
}
public async acquireSession(endpoint: string, username: string, password: string, additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataSession> {
const session = new AzdataSession();
session.sessionEnded().then(async () => {
// Wait for all commands running for this session to end
while (this._currentlyExecutingCommands.length > 0) {
await this._currentlyExecutingCommands[0].promise;
}
this._currentSession = undefined;
// Start our next command now that we're all done with this session
// TODO: Should we check if the command has a session that hasn't started? That should never happen..
// TODO: Look into kicking off multiple commands
this._queuedCommands.shift()?.deferred.resolve();
});
// We're not in a session or waiting on anything so just set the current session right now
if (!this._currentSession && this._queuedCommands.length === 0) {
this._currentSession = session;
} else { } else {
// We're in a session or another command is executing so add this to the end of the queued commands and wait our turn throw new Error(loc.endpointOrNamespaceRequired);
const deferred = new Deferred<void>();
deferred.promise.then(() => {
this._currentSession = session;
// We've started a new session so look at all our queued commands and start
// the ones for this session now.
this._queuedCommands = this._queuedCommands.filter(c => {
if (c.session === this._currentSession) {
c.deferred.resolve();
return false;
}
return true;
});
});
this._queuedCommands.push({ deferred, session: undefined });
await deferred.promise;
} }
return this.executeCommand<void>(args, Object.assign({}, additionalEnvVars, { 'AZDATA_PASSWORD': password }), azdataContext);
await this.login(endpoint, username, password, additionalEnvVars);
return session;
} }
/** /**
@@ -265,34 +213,16 @@ export class AzdataTool implements azdataExt.IAzdataApi {
}; };
} }
public async executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<R>> {
if (this._currentSession && this._currentSession !== session) {
const deferred = new Deferred<void>();
this._queuedCommands.push({ deferred, session: session });
await deferred.promise;
}
const executingDeferred = new Deferred<void>();
this._currentlyExecutingCommands.push(executingDeferred);
try {
return await this.executeCommandImpl<R>(args, additionalEnvVars);
}
finally {
this._currentlyExecutingCommands = this._currentlyExecutingCommands.filter(c => c !== executingDeferred);
executingDeferred.resolve();
// If there isn't an active session and we still have queued commands then we have to manually kick off the next one
if (this._queuedCommands.length > 0 && !this._currentSession) {
this._queuedCommands.shift()?.deferred.resolve();
}
}
}
/** /**
* Executes the specified azdata command. This is NOT session-aware so should only be used for calls that don't care about a session * Executes the specified azdata command.
* @param args The args to pass to azdata * @param args The args to pass to azdata
* @param additionalEnvVars Additional environment variables to set for this execution * @param additionalEnvVars Additional environment variables to set for this execution
*/ */
private async executeCommandImpl<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<R>> { public async executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<R>> {
try { try {
if (azdataContext) {
args = args.concat('--controller-context', azdataContext);
}
const output = JSON.parse((await executeAzdataCommand(`"${this._path}"`, args.concat(['--output', 'json']), additionalEnvVars)).stdout); const output = JSON.parse((await executeAzdataCommand(`"${this._path}"`, args.concat(['--output', 'json']), additionalEnvVars)).stdout);
return { return {
logs: <string[]>output.log, logs: <string[]>output.log,
@@ -442,8 +372,22 @@ export async function checkAndInstallAzdata(userRequested: boolean = false): Pro
export async function checkAndUpdateAzdata(currentAzdata?: IAzdataTool, userRequested: boolean = false): Promise<boolean> { export async function checkAndUpdateAzdata(currentAzdata?: IAzdataTool, userRequested: boolean = false): Promise<boolean> {
if (currentAzdata !== undefined) { if (currentAzdata !== undefined) {
const newSemVersion = await discoverLatestAvailableAzdataVersion(); const newSemVersion = await discoverLatestAvailableAzdataVersion();
if (newSemVersion.compare(await currentAzdata.getSemVersion()) === 1) { const currentSemVersion = await currentAzdata.getSemVersion();
Logger.log(loc.foundAzdataVersionToUpdateTo(newSemVersion.raw, (await currentAzdata.getSemVersion()).raw)); Logger.log(loc.foundAzdataVersionToUpdateTo(newSemVersion.raw, currentSemVersion.raw));
if (MIN_AZDATA_VERSION.compare(currentSemVersion) === 1) {
if (newSemVersion.compare(MIN_AZDATA_VERSION) >= 0) {
return await promptToUpdateAzdata(newSemVersion.raw, userRequested, true);
} else {
// This should never happen - it means that the currently available version to download
// is < the version we require. If this was to happen it'd imply something is wrong with
// the version JSON or the minimum required version.
// Regardless, there's nothing we can do and so we just bail out at this point and tell the user
// they have to install it manually (hopefully it's available and wasn't a publishing mistake)
vscode.window.showInformationMessage(loc.requiredVersionNotAvailable(MIN_AZDATA_VERSION.raw, newSemVersion.raw));
Logger.log(loc.requiredVersionNotAvailable(newSemVersion.raw, currentSemVersion.raw));
}
}
else if (newSemVersion.compare(currentSemVersion) === 1) {
return await promptToUpdateAzdata(newSemVersion.raw, userRequested); return await promptToUpdateAzdata(newSemVersion.raw, userRequested);
} else { } else {
Logger.log(loc.currentlyInstalledVersionIsLatest((await currentAzdata.getSemVersion()).raw)); Logger.log(loc.currentlyInstalledVersionIsLatest((await currentAzdata.getSemVersion()).raw));
@@ -504,39 +448,65 @@ async function promptToInstallAzdata(userRequested: boolean = false): Promise<bo
* @param newVersion - provides the new version that the user will be prompted to update to * @param newVersion - provides the new version that the user will be prompted to update to
* @param userRequested - if true this operation was requested in response to a user issued command, if false it was issued at startup by system * @param userRequested - if true this operation was requested in response to a user issued command, if false it was issued at startup by system
* returns true if update was done and false otherwise. * returns true if update was done and false otherwise.
* @param required - Whether this update is required. If true then we will always show the prompt and warn the user if they decline it
*/ */
async function promptToUpdateAzdata(newVersion: string, userRequested: boolean = false): Promise<boolean> { async function promptToUpdateAzdata(newVersion: string, userRequested: boolean = false, required = false): Promise<boolean> {
let response: string | undefined = loc.yes; if (required) {
const config = <AzdataDeployOption>getConfig(azdataUpdateKey); let response: string | undefined = loc.yes;
if (userRequested) {
Logger.show(); const responses = [loc.yes, loc.no];
Logger.log(loc.userRequestedUpdate); Logger.log(loc.promptForRequiredAzdataUpdateLog(MIN_AZDATA_VERSION.raw, newVersion));
} response = await vscode.window.showInformationMessage(loc.promptForRequiredAzdataUpdate(MIN_AZDATA_VERSION.raw, newVersion), ...responses);
if (config === AzdataDeployOption.dontPrompt && !userRequested) {
Logger.log(loc.skipUpdate(config));
return false;
}
const responses = userRequested
? [loc.yes, loc.no]
: [loc.yes, loc.askLater, loc.doNotAskAgain];
if (config === AzdataDeployOption.prompt) {
Logger.log(loc.promptForAzdataUpdateLog(newVersion));
response = await vscode.window.showInformationMessage(loc.promptForAzdataUpdate(newVersion), ...responses);
Logger.log(loc.userResponseToUpdatePrompt(response)); Logger.log(loc.userResponseToUpdatePrompt(response));
} if (response === loc.yes) {
if (response === loc.doNotAskAgain) { try {
await setConfig(azdataUpdateKey, AzdataDeployOption.dontPrompt); await updateAzdata();
} else if (response === loc.yes) { vscode.window.showInformationMessage(loc.azdataUpdated(newVersion));
try { Logger.log(loc.azdataUpdated(newVersion));
await updateAzdata(); return true;
vscode.window.showInformationMessage(loc.azdataUpdated(newVersion)); } catch (err) {
Logger.log(loc.azdataUpdated(newVersion)); // Windows: 1602 is User cancelling installation/update - not unexpected so don't display
return true; if (!(err instanceof ExitCodeError) || err.code !== 1602) {
} catch (err) { vscode.window.showWarningMessage(loc.updateError(err));
// Windows: 1602 is User cancelling installation/update - not unexpected so don't display Logger.log(loc.updateError(err));
if (!(err instanceof ExitCodeError) || err.code !== 1602) { }
vscode.window.showWarningMessage(loc.updateError(err)); }
Logger.log(loc.updateError(err)); } else {
vscode.window.showWarningMessage(loc.missingRequiredVersion(MIN_AZDATA_VERSION.raw));
}
} else {
let response: string | undefined = loc.yes;
const config = <AzdataDeployOption>getConfig(azdataUpdateKey);
if (userRequested) {
Logger.show();
Logger.log(loc.userRequestedUpdate);
}
if (config === AzdataDeployOption.dontPrompt && !userRequested) {
Logger.log(loc.skipUpdate(config));
return false;
}
const responses = userRequested
? [loc.yes, loc.no]
: [loc.yes, loc.askLater, loc.doNotAskAgain];
if (config === AzdataDeployOption.prompt) {
Logger.log(loc.promptForAzdataUpdateLog(newVersion));
response = await vscode.window.showInformationMessage(loc.promptForAzdataUpdate(newVersion), ...responses);
Logger.log(loc.userResponseToUpdatePrompt(response));
}
if (response === loc.doNotAskAgain) {
await setConfig(azdataUpdateKey, AzdataDeployOption.dontPrompt);
} else if (response === loc.yes) {
try {
await updateAzdata();
vscode.window.showInformationMessage(loc.azdataUpdated(newVersion));
Logger.log(loc.azdataUpdated(newVersion));
return true;
} catch (err) {
// Windows: 1602 is User cancelling installation/update - not unexpected so don't display
if (!(err instanceof ExitCodeError) || err.code !== 1602) {
vscode.window.showWarningMessage(loc.updateError(err));
Logger.log(loc.updateError(err));
}
} }
} }
} }

View File

@@ -38,8 +38,11 @@ export const promptLog = (logEntry: string) => localize('azdata.promptLog', "Pro
export const promptForAzdataInstall = localize('azdata.couldNotFindAzdataWithPrompt', "Could not find Azure Data CLI, install it now? If not then some features will not be able to function."); export const promptForAzdataInstall = localize('azdata.couldNotFindAzdataWithPrompt', "Could not find Azure Data CLI, install it now? If not then some features will not be able to function.");
export const promptForAzdataInstallLog = promptLog(promptForAzdataInstall); export const promptForAzdataInstallLog = promptLog(promptForAzdataInstall);
export const promptForAzdataUpdate = (version: string): string => localize('azdata.promptForAzdataUpdate', "A new version of Azure Data CLI ( {0} ) is available, do you wish to update to it now?", version); export const promptForAzdataUpdate = (version: string): string => localize('azdata.promptForAzdataUpdate', "A new version of Azure Data CLI ( {0} ) is available, do you wish to update to it now?", version);
export const promptForRequiredAzdataUpdate = (requiredVersion: string, latestVersion: string): string => localize('azdata.promptForRequiredAzdataUpdate', "This extension requires Azure Data CLI >= {0} to be installed, do you wish to update to the latest version ({1}) now? If you do not then some functionality may not work.", requiredVersion, latestVersion);
export const requiredVersionNotAvailable = (requiredVersion: string, currentVersion: string): string => localize('azdata.requiredVersionNotAvailable', "This extension requires Azure Data CLI >= {0} to be installed, but the current version available is only {1}. Install the correct version manually from [here](https://docs.microsoft.com/sql/azdata/install/deploy-install-azdata) and then restart Azure Data Studio.", requiredVersion, currentVersion);
export const promptForAzdataUpdateLog = (version: string): string => promptLog(promptForAzdataUpdate(version)); export const promptForAzdataUpdateLog = (version: string): string => promptLog(promptForAzdataUpdate(version));
export const promptForRequiredAzdataUpdateLog = (requiredVersion: string, latestVersion: string): string => promptLog(promptForRequiredAzdataUpdate(requiredVersion, latestVersion));
export const missingRequiredVersion = (requiredVersion: string): string => localize('azdata.missingRequiredVersion', "Azure Data CLI >= {0} is required for this extension to function, some features may not work correctly until that version or higher is installed.", requiredVersion);
export const downloadError = localize('azdata.downloadError', "Error while downloading"); export const downloadError = localize('azdata.downloadError', "Error while downloading");
export const installError = (err: any): string => localize('azdata.installError', "Error installing Azure Data CLI: {0}", err.message ?? err); export const installError = (err: any): string => localize('azdata.installError', "Error installing Azure Data CLI: {0}", err.message ?? err);
export const updateError = (err: any): string => localize('azdata.updateError', "Error updating Azure Data CLI: {0}", err.message ?? err); export const updateError = (err: any): string => localize('azdata.updateError', "Error updating Azure Data CLI: {0}", err.message ?? err);
@@ -66,3 +69,4 @@ export const promptForEula = (privacyStatementUrl: string, eulaUrl: string) => l
export const promptForEulaLog = (privacyStatementUrl: string, eulaUrl: string) => promptLog(promptForEula(privacyStatementUrl, eulaUrl)); export const promptForEulaLog = (privacyStatementUrl: string, eulaUrl: string) => promptLog(promptForEula(privacyStatementUrl, eulaUrl));
export const userResponseToEulaPrompt = (response: string | undefined) => localize('azdata.promptForEulaResponse', "User response to EULA prompt: {0}", response); export const userResponseToEulaPrompt = (response: string | undefined) => localize('azdata.promptForEulaResponse', "User response to EULA prompt: {0}", response);
export const eulaAcceptedStateOnStartup = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateOnStartup', "'EULA Accepted' state on startup: {0}", eulaAccepted); export const eulaAcceptedStateOnStartup = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateOnStartup', "'EULA Accepted' state on startup: {0}", eulaAccepted);
export const endpointOrNamespaceRequired = localize('azdata.endpointOrNamespaceRequired', "Either an endpoint or a namespace must be specified");

View File

@@ -51,7 +51,7 @@ describe('api', function (): void {
it('succeed when azdata present and EULA accepted', async function (): Promise<void> { it('succeed when azdata present and EULA accepted', async function (): Promise<void> {
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>(); const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
mementoMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => true); mementoMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => true);
const azdataTool = new AzdataTool('', '1.0.0'); const azdataTool = new AzdataTool('', '99.0.0');
const azdataToolService = new AzdataToolService(); const azdataToolService = new AzdataToolService();
azdataToolService.localAzdata = azdataTool; azdataToolService.localAzdata = azdataTool;
// Not using a mock here because it'll hang when resolving mocked objects // Not using a mock here because it'll hang when resolving mocked objects
@@ -60,7 +60,7 @@ describe('api', function (): void {
sinon.stub(childProcess, 'executeCommand').callsFake(async (_command, args) => { sinon.stub(childProcess, 'executeCommand').callsFake(async (_command, args) => {
// Version needs to be valid so it can be parsed correctly // Version needs to be valid so it can be parsed correctly
if (args[0] === '--version') { if (args[0] === '--version') {
return { stdout: `1.0.0`, stderr: '' }; return { stdout: `99.0.0`, stderr: '' };
} }
console.log(args[0]); console.log(args[0]);
return { stdout: `{ }`, stderr: '' }; return { stdout: `{ }`, stderr: '' };
@@ -96,15 +96,8 @@ describe('api', function (): void {
async function assertApiCalls(api: azdataExt.IExtension, assertCallback: (promise: Promise<any>, message: string) => Promise<void>): Promise<void> { async function assertApiCalls(api: azdataExt.IExtension, assertCallback: (promise: Promise<any>, message: string) => Promise<void>): Promise<void> {
await assertCallback(api.azdata.getPath(), 'getPath'); await assertCallback(api.azdata.getPath(), 'getPath');
await assertCallback(api.azdata.getSemVersion(), 'getSemVersion'); await assertCallback(api.azdata.getSemVersion(), 'getSemVersion');
await assertCallback(api.azdata.login('', '', ''), 'login'); await assertCallback(api.azdata.login({ endpoint: 'https://127.0.0.1' }, '', ''), 'login');
await assertCallback((async () => { await assertCallback(api.azdata.login({ namespace: 'namespace' }, '', ''), 'login');
let session: azdataExt.AzdataSession | undefined;
try {
session = await api.azdata.acquireSession('', '', '');
} finally {
session?.dispose();
}
})(), 'acquireSession');
await assertCallback(api.azdata.version(), 'version'); await assertCallback(api.azdata.version(), 'version');
await assertCallback(api.azdata.arc.dc.create('', '', '', '', '', ''), 'arc dc create'); await assertCallback(api.azdata.arc.dc.create('', '', '', '', '', ''), 'arc dc create');
@@ -117,7 +110,7 @@ describe('api', function (): void {
await assertCallback(api.azdata.arc.sql.mi.list(), 'arc sql mi list'); await assertCallback(api.azdata.arc.sql.mi.list(), 'arc sql mi list');
await assertCallback(api.azdata.arc.sql.mi.delete(''), 'arc sql mi delete'); await assertCallback(api.azdata.arc.sql.mi.delete(''), 'arc sql mi delete');
await assertCallback(api.azdata.arc.sql.mi.show(''), 'arc sql mi show'); await assertCallback(api.azdata.arc.sql.mi.show(''), 'arc sql mi show');
await assertCallback(api.azdata.arc.sql.mi.edit('', { }), 'arc sql mi edit'); await assertCallback(api.azdata.arc.sql.mi.edit('', {}), 'arc sql mi edit');
await assertCallback(api.azdata.arc.postgres.server.list(), 'arc sql postgres server list'); await assertCallback(api.azdata.arc.postgres.server.list(), 'arc sql postgres server list');
await assertCallback(api.azdata.arc.postgres.server.delete(''), 'arc sql postgres server delete'); await assertCallback(api.azdata.arc.postgres.server.delete(''), 'arc sql postgres server delete');
await assertCallback(api.azdata.arc.postgres.server.show(''), 'arc sql postgres server show'); await assertCallback(api.azdata.arc.postgres.server.show(''), 'arc sql postgres server show');

View File

@@ -3,7 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as azdataExt from 'azdata-ext';
import * as should from 'should'; import * as should from 'should';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
@@ -17,9 +16,8 @@ import * as fs from 'fs';
import { AzdataReleaseInfo } from '../azdataReleaseInfo'; import { AzdataReleaseInfo } from '../azdataReleaseInfo';
import * as TypeMoq from 'typemoq'; import * as TypeMoq from 'typemoq';
import { eulaAccepted } from '../constants'; import { eulaAccepted } from '../constants';
import { sleep } from './testUtils';
const oldAzdataMock = new azdata.AzdataTool('/path/to/azdata', '0.0.0'); const oldAzdataMock = new azdata.AzdataTool('/path/to/azdata', azdata.MIN_AZDATA_VERSION.raw);
const currentAzdataMock = new azdata.AzdataTool('/path/to/azdata', '9999.999.999'); const currentAzdataMock = new azdata.AzdataTool('/path/to/azdata', '9999.999.999');
/** /**
@@ -222,120 +220,10 @@ describe('azdata', function () {
const endpoint = 'myEndpoint'; const endpoint = 'myEndpoint';
const username = 'myUsername'; const username = 'myUsername';
const password = 'myPassword'; const password = 'myPassword';
await azdataTool.login(endpoint, username, password); await azdataTool.login({ endpoint: endpoint }, username, password);
verifyExecuteCommandCalledWithArgs(['login', endpoint, username]); verifyExecuteCommandCalledWithArgs(['login', endpoint, username]);
}); });
describe('acquireSession', function (): void {
it('calls login', async function (): Promise<void> {
const endpoint = 'myEndpoint';
const username = 'myUsername';
const password = 'myPassword';
const session = await azdataTool.acquireSession(endpoint, username, password);
session.dispose();
verifyExecuteCommandCalledWithArgs(['login', endpoint, username]);
});
it('command executed under current session completes', async function (): Promise<void> {
const session = await azdataTool.acquireSession('', '', '');
try {
await azdataTool.arc.dc.config.show(undefined, session);
} finally {
session.dispose();
}
verifyExecuteCommandCalledWithArgs(['login'], 0);
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
});
it('multiple commands executed under current session completes', async function (): Promise<void> {
const session = await azdataTool.acquireSession('', '', '');
try {
// Kick off multiple commands at the same time and then ensure that they both complete
await Promise.all([
azdataTool.arc.dc.config.show(undefined, session),
azdataTool.arc.sql.mi.list(undefined, session)
]);
} finally {
session.dispose();
}
verifyExecuteCommandCalledWithArgs(['login'], 0);
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list'], 2);
});
it('command executed without session context is queued up until session is closed', async function (): Promise<void> {
const session = await azdataTool.acquireSession('', '', '');
let nonSessionCommand: Promise<any> | undefined = undefined;
try {
// Start one command in the current session
await azdataTool.arc.dc.config.show(undefined, session);
// Verify that the command isn't executed until after the session is disposed
let isFulfilled = false;
nonSessionCommand = azdataTool.arc.sql.mi.list().then(() => isFulfilled = true);
await sleep(2000);
should(isFulfilled).equal(false, 'The command should not be completed yet');
} finally {
session.dispose();
}
await nonSessionCommand;
verifyExecuteCommandCalledWithArgs(['login'], 0);
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list'], 2);
});
it('multiple commands executed without session context are queued up until session is closed', async function (): Promise<void> {
const session = await azdataTool.acquireSession('', '', '');
let nonSessionCommand1: Promise<any> | undefined = undefined;
let nonSessionCommand2: Promise<any> | undefined = undefined;
try {
// Start one command in the current session
await azdataTool.arc.dc.config.show(undefined, session);
// Verify that neither command is completed until the session is closed
let isFulfilled = false;
nonSessionCommand1 = azdataTool.arc.sql.mi.list().then(() => isFulfilled = true);
nonSessionCommand2 = azdataTool.arc.postgres.server.list().then(() => isFulfilled = true);
await sleep(2000);
should(isFulfilled).equal(false, 'The commands should not be completed yet');
} finally {
session.dispose();
}
await Promise.all([nonSessionCommand1, nonSessionCommand2]);
verifyExecuteCommandCalledWithArgs(['login'], 0);
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list'], 2);
verifyExecuteCommandCalledWithArgs(['arc', 'postgres', 'server', 'list'], 3);
});
it('attempting to acquire a second session while a first is still active queues the second session', async function (): Promise<void> {
const firstSession = await azdataTool.acquireSession('', '', '');
let sessionPromise: Promise<azdataExt.AzdataSession> | undefined = undefined;
let secondSessionCommand: Promise<any> | undefined = undefined;
try {
try {
// Start one command in the current session
await azdataTool.arc.dc.config.show(undefined, firstSession);
// Verify that none of the commands for the second session are completed before the first is disposed
let isFulfilled = false;
sessionPromise = azdataTool.acquireSession('', '', '');
sessionPromise.then(session => {
isFulfilled = true;
secondSessionCommand = azdataTool.arc.sql.mi.list(undefined, session).then(() => isFulfilled = true);
});
await sleep(2000);
should(isFulfilled).equal(false, 'The commands should not be completed yet');
} finally {
firstSession.dispose();
}
} finally {
(await sessionPromise)?.dispose();
}
should(secondSessionCommand).not.equal(undefined, 'The second command should have been queued already');
await secondSessionCommand!;
verifyExecuteCommandCalledWithArgs(['login'], 0);
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show'], 1);
verifyExecuteCommandCalledWithArgs(['login'], 2);
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list'], 3);
});
});
it('version', async function (): Promise<void> { it('version', async function (): Promise<void> {
executeCommandStub.resolves({ stdout: '1.0.0', stderr: '' }); executeCommandStub.resolves({ stdout: '1.0.0', stderr: '' });
await azdataTool.version(); await azdataTool.version();
@@ -777,7 +665,7 @@ async function testDarwinSkippedUpdateDontPrompt() {
async function testWin32SkippedUpdateDontPrompt() { async function testWin32SkippedUpdateDontPrompt() {
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename)); sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
await azdata.checkAndUpdateAzdata(oldAzdataMock); await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(executeSudoCommandStub.notCalled).be.true('executeSudoCommand should not have been called'); should(executeSudoCommandStub.notCalled).be.true(`executeSudoCommand should not have been called ${executeSudoCommandStub.getCalls().join(os.EOL)}`);
} }
async function testLinuxSkippedUpdateDontPrompt() { async function testLinuxSkippedUpdateDontPrompt() {

View File

@@ -18,7 +18,3 @@ export async function assertRejected(promise: Promise<any>, message: string): Pr
throw new Error(message); throw new Error(message);
} }
export async function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@@ -160,7 +160,7 @@ declare module 'azdata-ext' {
export interface PostgresServerShowResult { export interface PostgresServerShowResult {
apiVersion: string, // "arcdata.microsoft.com/v1alpha1" apiVersion: string, // "arcdata.microsoft.com/v1alpha1"
kind: string, // "postgresql-12" kind: string, // "postgresql"
metadata: { metadata: {
creationTimestamp: string, // "2020-08-19T20:25:11Z" creationTimestamp: string, // "2020-08-19T20:25:11Z"
generation: number, // 1 generation: number, // 1
@@ -177,7 +177,8 @@ declare module 'azdata-ext' {
}[], }[],
settings: { settings: {
default: { [key: string]: string } // { "max_connections": "101", "work_mem": "4MB" } default: { [key: string]: string } // { "max_connections": "101", "work_mem": "4MB" }
} },
version: string // "12"
}, },
scale: { scale: {
shards: number, // 1 (shards was renamed to workers, kept here for backwards compatibility) shards: number, // 1 (shards was renamed to workers, kept here for backwards compatibility)
@@ -244,25 +245,27 @@ declare module 'azdata-ext' {
code?: number code?: number
} }
export interface AzdataSession extends vscode.Disposable { } export interface EndpointOrNamespace {
endpoint?: string,
namespace?: string
}
export interface IAzdataApi { export interface IAzdataApi {
arc: { arc: {
dc: { dc: {
create(namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<void>>, create(namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
endpoint: { endpoint: {
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcEndpointListResult[]>> list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcEndpointListResult[]>>
}, },
config: { config: {
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcConfigListResult[]>>, list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcConfigListResult[]>>,
show(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcConfigShowResult>> show(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcConfigShowResult>>
} }
}, },
postgres: { postgres: {
server: { server: {
delete(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<void>>, delete(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<PostgresServerListResult[]>>, list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<PostgresServerListResult[]>>,
show(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<PostgresServerShowResult>>, show(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<PostgresServerShowResult>>,
edit( edit(
name: string, name: string,
args: { args: {
@@ -278,17 +281,16 @@ declare module 'azdata-ext' {
replaceEngineSettings?: boolean, replaceEngineSettings?: boolean,
workers?: number workers?: number
}, },
engineVersion?: string,
additionalEnvVars?: AdditionalEnvVars, additionalEnvVars?: AdditionalEnvVars,
session?: AzdataSession azdataContext?: string
): Promise<AzdataOutput<void>> ): Promise<AzdataOutput<void>>
} }
}, },
sql: { sql: {
mi: { mi: {
delete(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<void>>, delete(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<SqlMiListResult[]>>, list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<SqlMiListResult[]>>,
show(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<SqlMiShowResult>>, show(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<SqlMiShowResult>>,
edit( edit(
name: string, name: string,
args: { args: {
@@ -299,22 +301,13 @@ declare module 'azdata-ext' {
noWait?: boolean, noWait?: boolean,
}, },
additionalEnvVars?: AdditionalEnvVars, additionalEnvVars?: AdditionalEnvVars,
session?: AzdataSession azdataContext?: string
): Promise<AzdataOutput<void>> ): Promise<AzdataOutput<void>>
} }
} }
}, },
getPath(): Promise<string>, getPath(): Promise<string>,
login(endpoint: string, username: string, password: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzdataOutput<void>>, login(endpointOrNamespace: EndpointOrNamespace, username: string, password: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
/**
* Acquires a session for the specified controller, which will log in to the specified controller and then block all other commands
* that are not part of the original session from executing until the session is released (disposed).
* @param endpoint
* @param username
* @param password
* @param additionalEnvVars
*/
acquireSession(endpoint: string, username: string, password: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzdataSession>,
/** /**
* The semVersion corresponding to this installation of azdata. version() method should have been run * The semVersion corresponding to this installation of azdata. version() method should have been run
* before fetching this value to ensure that correct value is returned. This is almost always correct unless * before fetching this value to ensure that correct value is returned. This is almost always correct unless

View File

@@ -248,7 +248,7 @@
}, },
{ {
"name": "azdata-old", "name": "azdata-old",
"version": "20.3.1" "version": "20.3.2"
} }
], ],
"when": "target=new-aks&&version=bdc2019" "when": "target=new-aks&&version=bdc2019"
@@ -266,7 +266,7 @@
}, },
{ {
"name": "azdata-old", "name": "azdata-old",
"version": "20.3.1" "version": "20.3.2"
} }
], ],
"when": "target=existing-aks&&version=bdc2019" "when": "target=existing-aks&&version=bdc2019"
@@ -284,7 +284,7 @@
}, },
{ {
"name": "azdata-old", "name": "azdata-old",
"version": "20.3.1" "version": "20.3.2"
} }
], ],
"when": "target=existing-kubeadm&&version=bdc2019" "when": "target=existing-kubeadm&&version=bdc2019"
@@ -302,7 +302,7 @@
}, },
{ {
"name": "azdata-old", "name": "azdata-old",
"version": "20.3.1" "version": "20.3.2"
} }
], ],
"when": "target=existing-aro&&version=bdc2019" "when": "target=existing-aro&&version=bdc2019"
@@ -320,7 +320,7 @@
}, },
{ {
"name": "azdata-old", "name": "azdata-old",
"version": "20.3.1" "version": "20.3.2"
} }
], ],
"when": "target=existing-openshift&&version=bdc2019" "when": "target=existing-openshift&&version=bdc2019"

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.1328 0.296875C10.9974 0.53125 11.7891 0.898438 12.5078 1.39844C13.2266 1.89323 13.8438 2.48177 14.3594 3.16406C14.8802 3.84115 15.2839 4.59375 15.5703 5.42188C15.8568 6.24479 16 7.10417 16 8C16 8.73438 15.9036 9.44271 15.7109 10.125C15.5234 10.8073 15.2552 11.4453 14.9062 12.0391C14.5625 12.6328 14.1458 13.1745 13.6562 13.6641C13.1719 14.1484 12.6328 14.5651 12.0391 14.9141C11.4453 15.2578 10.8073 15.526 10.125 15.7188C9.44271 15.9062 8.73438 16 8 16C7.26562 16 6.55729 15.9062 5.875 15.7188C5.19271 15.526 4.55469 15.2578 3.96094 14.9141C3.36719 14.5651 2.82552 14.1484 2.33594 13.6641C1.85156 13.1745 1.4349 12.6328 1.08594 12.0391C0.742188 11.4453 0.473958 10.8099 0.28125 10.1328C0.09375 9.45052 0 8.73958 0 8C0 7.27083 0.0963542 6.5625 0.289062 5.875C0.481771 5.1875 0.755208 4.54167 1.10938 3.9375C1.46875 3.32812 1.90365 2.77604 2.41406 2.28125C2.92448 1.78125 3.5 1.35417 4.14062 1H2V0H6V4H5V1.67969C4.39062 1.97135 3.83854 2.33854 3.34375 2.78125C2.85417 3.21875 2.4349 3.71354 2.08594 4.26562C1.73698 4.8125 1.46875 5.40365 1.28125 6.03906C1.09375 6.67448 1 7.32812 1 8C1 8.64062 1.08333 9.26042 1.25 9.85938C1.41667 10.4531 1.65104 11.0104 1.95312 11.5312C2.26042 12.0469 2.6276 12.5182 3.05469 12.9453C3.48177 13.3724 3.95312 13.7396 4.46875 14.0469C4.98958 14.349 5.54688 14.5833 6.14062 14.75C6.73438 14.9167 7.35417 15 8 15C8.64062 15 9.25781 14.9167 9.85156 14.75C10.4505 14.5833 11.0078 14.349 11.5234 14.0469C12.0443 13.7396 12.5182 13.3724 12.9453 12.9453C13.3724 12.5182 13.737 12.0469 14.0391 11.5312C14.3464 11.0104 14.5833 10.4531 14.75 9.85938C14.9167 9.26562 15 8.64583 15 8C15 7.21875 14.8724 6.46615 14.6172 5.74219C14.3672 5.01823 14.0156 4.35938 13.5625 3.76562C13.1094 3.17188 12.5677 2.65885 11.9375 2.22656C11.3125 1.78906 10.6224 1.46615 9.86719 1.25781L10.1328 0.296875Z" fill="#0078D4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -23,6 +23,7 @@ export const DoNotShowAgain = localize('dataworkspace.doNotShowAgain', "Do not s
export const ProjectsFailedToLoad = localize('dataworkspace.projectsFailedToLoad', "Some projects failed to load. Please open console for more information"); export const ProjectsFailedToLoad = localize('dataworkspace.projectsFailedToLoad', "Some projects failed to load. Please open console for more information");
export const fileDoesNotExist = (name: string): string => { return localize('fileDoesNotExist', "File '{0}' doesn't exist", name); }; export const fileDoesNotExist = (name: string): string => { return localize('fileDoesNotExist', "File '{0}' doesn't exist", name); };
export const projectNameNull = localize('projectNameNull', "Project name is null"); export const projectNameNull = localize('projectNameNull', "Project name is null");
export const noPreviousData = (tableName: string): string => { return localize('noPreviousData', "Prior {0} for the current project will appear here, please run to see the results.", tableName); };
// config settings // config settings
export const projectsConfigurationKey = 'projects'; export const projectsConfigurationKey = 'projects';
@@ -75,6 +76,9 @@ export const LocalClonePathPlaceholder = localize('dataworkspace.localClonePathP
export const ProjectConfigurationKey = 'projects'; export const ProjectConfigurationKey = 'projects';
export const ProjectSaveLocationKey = 'defaultProjectSaveLocation'; export const ProjectSaveLocationKey = 'defaultProjectSaveLocation';
// Dashboard dialog
export const Refresh = localize('dataworksapce.refresh', 'Refresh');
export namespace cssStyles { export namespace cssStyles {
export const title = { 'font-size': '18px', 'font-weight': '600' }; export const title = { 'font-size': '18px', 'font-weight': '600' };
export const tableHeader = { 'text-align': 'left', 'font-weight': '500', 'font-size': '13px', 'user-select': 'text' }; export const tableHeader = { 'text-align': 'left', 'font-weight': '500', 'font-size': '13px', 'user-select': 'text' };

View File

@@ -13,11 +13,13 @@ export interface IconPath {
export class IconPathHelper { export class IconPathHelper {
private static extensionContext: vscode.ExtensionContext; private static extensionContext: vscode.ExtensionContext;
public static folder: IconPath; public static folder: IconPath;
public static refresh: IconPath;
public static setExtensionContext(extensionContext: vscode.ExtensionContext) { public static setExtensionContext(extensionContext: vscode.ExtensionContext) {
IconPathHelper.extensionContext = extensionContext; IconPathHelper.extensionContext = extensionContext;
IconPathHelper.folder = IconPathHelper.makeIcon('folder', true); IconPathHelper.folder = IconPathHelper.makeIcon('folder', true);
IconPathHelper.refresh = IconPathHelper.makeIcon('refresh', true);
} }
private static makeIcon(name: string, sameIcon: boolean = false) { private static makeIcon(name: string, sameIcon: boolean = false) {

View File

@@ -67,6 +67,11 @@ declare module 'dataworkspace' {
*/ */
createProject(name: string, location: vscode.Uri, projectTypeId: string): Promise<vscode.Uri>; createProject(name: string, location: vscode.Uri, projectTypeId: string): Promise<vscode.Uri>;
/**
* Gets the project data corresponding to the project file, to be placed in the dashboard container
*/
getDashboardComponents(projectFile: string): IDashboardTable[];
/** /**
* Gets the supported project types * Gets the supported project types
*/ */
@@ -77,11 +82,6 @@ declare module 'dataworkspace' {
*/ */
readonly projectActions: (IProjectAction | IProjectActionGroup)[]; readonly projectActions: (IProjectAction | IProjectActionGroup)[];
/**
* Gets the project data to be placed in the dashboard container
*/
readonly dashboardComponents: IDashboardTable[];
/** /**
* Gets the project image to be used as background in dashboard container * Gets the project image to be used as background in dashboard container
*/ */

View File

@@ -8,6 +8,7 @@ import { IDashboardColumnInfo, IDashboardTable, IProjectAction, IProjectActionGr
import * as path from 'path'; import * as path from 'path';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as constants from '../common/constants'; import * as constants from '../common/constants';
import { IconPathHelper } from '../common/iconHelper';
import { IWorkspaceService } from '../common/interfaces'; import { IWorkspaceService } from '../common/interfaces';
import { fileExist } from '../common/utils'; import { fileExist } from '../common/utils';
@@ -17,6 +18,8 @@ export class ProjectDashboard {
private modelView: azdata.ModelView | undefined; private modelView: azdata.ModelView | undefined;
private projectProvider: IProjectProvider | undefined; private projectProvider: IProjectProvider | undefined;
private overviewTab: azdata.DashboardTab | undefined; private overviewTab: azdata.DashboardTab | undefined;
private rootContainer: azdata.FlexContainer | undefined;
private tableContainer: azdata.Component | undefined;
constructor(private _workspaceService: IWorkspaceService, private _treeItem: WorkspaceTreeItem) { constructor(private _workspaceService: IWorkspaceService, private _treeItem: WorkspaceTreeItem) {
} }
@@ -41,7 +44,7 @@ export class ProjectDashboard {
await this.dashboard!.open(); await this.dashboard!.open();
} }
private async createDashboard(title: string, location: string): Promise<void> { private async createDashboard(title: string, projectFilePath: string): Promise<void> {
this.dashboard = azdata.window.createModelViewDashboard(title, 'ProjectDashboard', { alwaysShowTabs: false }); this.dashboard = azdata.window.createModelViewDashboard(title, 'ProjectDashboard', { alwaysShowTabs: false });
this.dashboard.registerTabs(async (modelView: azdata.ModelView) => { this.dashboard.registerTabs(async (modelView: azdata.ModelView) => {
this.modelView = modelView; this.modelView = modelView;
@@ -49,8 +52,8 @@ export class ProjectDashboard {
this.overviewTab = { this.overviewTab = {
title: '', title: '',
id: 'overview-tab', id: 'overview-tab',
content: this.createContainer(title, location), content: this.createContainer(title, projectFilePath),
toolbar: this.createToolbarContainer() toolbar: this.createToolbarContainer(projectFilePath)
}; };
return [ return [
this.overviewTab this.overviewTab
@@ -58,7 +61,7 @@ export class ProjectDashboard {
}); });
} }
private createToolbarContainer(): azdata.ToolbarContainer { private createToolbarContainer(projectFilePath: string): azdata.ToolbarContainer {
const projectActions: (IProjectAction | IProjectActionGroup)[] = this.projectProvider!.projectActions; const projectActions: (IProjectAction | IProjectActionGroup)[] = this.projectProvider!.projectActions;
// Add actions as buttons // Add actions as buttons
@@ -69,17 +72,32 @@ export class ProjectDashboard {
projectActions.forEach((action, actionIndex) => { projectActions.forEach((action, actionIndex) => {
if (this.isProjectAction(action)) { if (this.isProjectAction(action)) {
const button = this.createButton(action); const button = this.createButton(action);
buttons.push({ component: button }); buttons.push({ component: button, toolbarSeparatorAfter: (projectActionsLength - 1 === actionIndex) });
} else { } else {
const groupLength = action.actions.length; const groupLength = action.actions.length;
action.actions.forEach((groupAction, index) => { action.actions.forEach((groupAction, index) => {
const button = this.createButton(groupAction); const button = this.createButton(groupAction);
buttons.push({ component: button, toolbarSeparatorAfter: ((groupLength - 1 === index) && (projectActionsLength - 1 !== actionIndex)) }); // Add toolbar separator at the end of the group, if the group is not the last in the list buttons.push({ component: button, toolbarSeparatorAfter: ((groupLength - 1 === index) || (projectActionsLength - 1 === actionIndex)) }); // Add toolbar separator at the end of the group
}); });
} }
}); });
const refreshButton = this.modelView!.modelBuilder.button()
.withProperties<azdata.ButtonProperties>({
label: constants.Refresh,
iconPath: IconPathHelper.refresh,
height: '20px'
}).component();
refreshButton.onDidClick(() => {
this.rootContainer?.removeItem(this.tableContainer!);
this.tableContainer = this.createTables(projectFilePath);
this.rootContainer?.addItem(this.tableContainer);
});
buttons.push({ component: refreshButton });
return this.modelView!.modelBuilder.toolbarContainer() return this.modelView!.modelBuilder.toolbarContainer()
.withToolbarItems( .withToolbarItems(
buttons buttons
@@ -105,21 +123,21 @@ export class ProjectDashboard {
return button; return button;
} }
private createContainer(title: string, location: string): azdata.FlexContainer { private createContainer(title: string, projectFilePath: string): azdata.FlexContainer {
const rootContainer = this.modelView!.modelBuilder.flexContainer().withLayout( this.rootContainer = this.modelView!.modelBuilder.flexContainer().withLayout(
{ {
flexFlow: 'column', flexFlow: 'column',
width: '100%', width: '100%',
height: '100%' height: '100%'
}).component(); }).component();
const headerContainer = this.createHeader(title, location); const headerContainer = this.createHeader(title, projectFilePath);
const tableContainer = this.createTables(); this.tableContainer = this.createTables(projectFilePath);
rootContainer.addItem(headerContainer); this.rootContainer.addItem(headerContainer);
rootContainer.addItem(tableContainer); this.rootContainer.addItem(this.tableContainer);
return rootContainer; return this.rootContainer;
} }
/** /**
@@ -170,8 +188,8 @@ export class ProjectDashboard {
/** /**
* Adds all the tables to the container * Adds all the tables to the container
*/ */
private createTables(): azdata.Component { private createTables(projectFile: string): azdata.Component {
const dashboardData: IDashboardTable[] = this.projectProvider!.dashboardComponents; const dashboardData: IDashboardTable[] = this.projectProvider!.getDashboardComponents(projectFile);
const tableContainer = this.modelView!.modelBuilder.flexContainer().withLayout( const tableContainer = this.modelView!.modelBuilder.flexContainer().withLayout(
{ {
@@ -186,54 +204,62 @@ export class ProjectDashboard {
.component(); .component();
tableContainer.addItem(tableNameLabel, { CSSStyles: { 'padding-left': '25px', 'padding-bottom': '20px', ...constants.cssStyles.title } }); tableContainer.addItem(tableNameLabel, { CSSStyles: { 'padding-left': '25px', 'padding-bottom': '20px', ...constants.cssStyles.title } });
const columns: azdata.DeclarativeTableColumn[] = []; if (info.data.length === 0) {
info.columns.forEach((column: IDashboardColumnInfo) => { const noDataText = constants.noPreviousData(info.name.toLocaleLowerCase());
let col = { const noDataLabel = this.modelView!.modelBuilder.text()
displayName: column.displayName, .withProperties<azdata.TextComponentProperties>({ value: noDataText })
valueType: column.type === 'icon' ? azdata.DeclarativeDataType.component : azdata.DeclarativeDataType.string, .component();
isReadOnly: true, tableContainer.addItem(noDataLabel, { CSSStyles: { 'padding-left': '25px', 'padding-bottom': '20px' } });
width: column.width, } else {
headerCssStyles: { const columns: azdata.DeclarativeTableColumn[] = [];
'border': 'none', info.columns.forEach((column: IDashboardColumnInfo) => {
...constants.cssStyles.tableHeader let col = {
}, displayName: column.displayName,
rowCssStyles: { valueType: column.type === 'icon' ? azdata.DeclarativeDataType.component : azdata.DeclarativeDataType.string,
...constants.cssStyles.tableRow isReadOnly: true,
}, width: column.width,
}; headerCssStyles: {
columns.push(col); 'border': 'none',
}); ...constants.cssStyles.tableHeader
},
const data: azdata.DeclarativeTableCellValue[][] = []; rowCssStyles: {
info.data.forEach(values => { ...constants.cssStyles.tableRow
const columnValue: azdata.DeclarativeTableCellValue[] = []; },
values.forEach(val => { };
if (typeof val === 'string') { columns.push(col);
columnValue.push({ value: val });
} else {
const iconComponent = this.modelView!.modelBuilder.image().withProperties<azdata.ImageComponentProperties>({
iconPath: val.icon,
width: '15px',
height: '15px',
iconHeight: '15px',
iconWidth: '15px'
}).component();
const stringComponent = this.modelView!.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: val.text,
CSSStyles: { 'margin-block-start': 'auto', 'block-size': 'auto', 'margin-block-end': '0px' }
}).component();
const columnData = this.modelView!.modelBuilder.flexContainer().withItems([iconComponent, stringComponent], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }).withLayout({ flexFlow: 'row' }).component();
columnValue.push({ value: columnData });
}
}); });
data.push(columnValue);
});
const table = this.modelView!.modelBuilder.declarativeTable() const data: azdata.DeclarativeTableCellValue[][] = [];
.withProperties<azdata.DeclarativeTableProperties>({ columns: columns, dataValues: data, ariaLabel: info.name, CSSStyles: { 'margin-left': '30px' } }).component(); info.data.forEach(values => {
const columnValue: azdata.DeclarativeTableCellValue[] = [];
values.forEach(val => {
if (typeof val === 'string') {
columnValue.push({ value: val });
} else {
const iconComponent = this.modelView!.modelBuilder.image().withProperties<azdata.ImageComponentProperties>({
iconPath: val.icon,
width: '15px',
height: '15px',
iconHeight: '15px',
iconWidth: '15px'
}).component();
const stringComponent = this.modelView!.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: val.text,
CSSStyles: { 'margin-block-start': 'auto', 'block-size': 'auto', 'margin-block-end': '0px' }
}).component();
tableContainer.addItem(table); const columnData = this.modelView!.modelBuilder.flexContainer().withItems([iconComponent, stringComponent], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }).withLayout({ flexFlow: 'row' }).component();
columnValue.push({ value: columnData });
}
});
data.push(columnValue);
});
const table = this.modelView!.modelBuilder.declarativeTable()
.withProperties<azdata.DeclarativeTableProperties>({ columns: columns, dataValues: data, ariaLabel: info.name, CSSStyles: { 'margin-left': '30px' } }).component();
tableContainer.addItem(table);
}
}); });
return tableContainer; return tableContainer;
} }

View File

@@ -33,7 +33,9 @@ export function createProjectProvider(projectTypes: IProjectType[], projectActio
return Promise.resolve(location); return Promise.resolve(location);
}, },
projectActions: projectActions, projectActions: projectActions,
dashboardComponents: dashboardComponents getDashboardComponents: (projectFile: string): IDashboardTable[] => {
return dashboardComponents;
}
}; };
return projectProvider; return projectProvider;
} }

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { IProjectProvider, WorkspaceTreeItem } from 'dataworkspace'; import { IDashboardTable, IProjectProvider, WorkspaceTreeItem } from 'dataworkspace';
import 'mocha'; import 'mocha';
import * as should from 'should'; import * as should from 'should';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
@@ -111,7 +111,8 @@ suite('workspaceTreeDataProvider Tests', function (): void {
id: 'Target Version', id: 'Target Version',
run: async (): Promise<any> => { return Promise.resolve(); } run: async (): Promise<any> => { return Promise.resolve(); }
}], }],
dashboardComponents: [{ getDashboardComponents: (projectFile: string): IDashboardTable[] => {
return [{
name: 'Deployments', name: 'Deployments',
columns: [{ displayName: 'c1', width: 75, type: 'string' }], columns: [{ displayName: 'c1', width: 75, type: 'string' }],
data: [['d1']] data: [['d1']]
@@ -120,8 +121,8 @@ suite('workspaceTreeDataProvider Tests', function (): void {
name: 'Builds', name: 'Builds',
columns: [{ displayName: 'c1', width: 75, type: 'string' }], columns: [{ displayName: 'c1', width: 75, type: 'string' }],
data: [['d1']] data: [['d1']]
}] }];
}; }};
const getProjectProviderStub = sinon.stub(workspaceService, 'getProjectProvider'); const getProjectProviderStub = sinon.stub(workspaceService, 'getProjectProvider');
getProjectProviderStub.onFirstCall().resolves(undefined); getProjectProviderStub.onFirstCall().resolves(undefined);
getProjectProviderStub.onSecondCall().resolves(projectProvider); getProjectProviderStub.onSecondCall().resolves(projectProvider);

View File

@@ -11,20 +11,21 @@ export async function deactivate(): Promise<any> {
} }
export async function activate(context: ExtensionContext): Promise<void> { export async function activate(context: ExtensionContext): Promise<void> {
context.subscriptions.push(commands.registerCommand('git.credential', async (data: any) => { context.subscriptions.push(commands.registerCommand('git.credential', async (_data: any) => {
try { return { stdout: '', stderr: '', code: 0 };
const { stdout, stderr } = await exec(`git credential ${data.command}`, { // try {
stdin: data.stdin, // const { stdout, stderr } = await exec(`git credential ${data.command}`, {
env: Object.assign(process.env, { GIT_TERMINAL_PROMPT: '0' }) // stdin: data.stdin,
}); // env: Object.assign(process.env, { GIT_TERMINAL_PROMPT: '0' })
return { stdout, stderr, code: 0 }; // });
} catch ({ stdout, stderr, error }) { // return { stdout, stderr, code: 0 };
const code = error.code || 0; // } catch ({ stdout, stderr, error }) {
if (stderr.indexOf('terminal prompts disabled') !== -1) { // const code = error.code || 0;
stderr = ''; // if (stderr.indexOf('terminal prompts disabled') !== -1) {
} // stderr = '';
return { stdout, stderr, code }; // }
} // return { stdout, stderr, code };
// }
})); }));
} }

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "kusto", "name": "kusto",
"version": "0.5.2", "version": "0.5.3",
"publisher": "Microsoft", "publisher": "Microsoft",
"aiKey": "AIF-444c3af9-8e69-4462-ab49-4191e6ad1916", "aiKey": "AIF-444c3af9-8e69-4462-ab49-4191e6ad1916",
"activationEvents": [ "activationEvents": [
@@ -8,7 +8,7 @@
], ],
"engines": { "engines": {
"vscode": "*", "vscode": "*",
"azdata": ">=1.27.0" "azdata": ">=1.28.0"
}, },
"main": "./out/main", "main": "./out/main",
"repository": { "repository": {

View File

@@ -56,6 +56,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
this.bookTocManager = new BookTocManager(); this.bookTocManager = new BookTocManager();
this._bookViewer = vscode.window.createTreeView(this.viewId, { showCollapseAll: true, treeDataProvider: this }); this._bookViewer = vscode.window.createTreeView(this.viewId, { showCollapseAll: true, treeDataProvider: this });
this._bookViewer.onDidChangeVisibility(async e => { this._bookViewer.onDidChangeVisibility(async e => {
await this.initialized;
// Whenever the viewer changes visibility then try and reveal the currently active document // Whenever the viewer changes visibility then try and reveal the currently active document
// in the tree view // in the tree view
let openDocument = azdata.nb.activeNotebookEditor; let openDocument = azdata.nb.activeNotebookEditor;
@@ -122,7 +123,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
} }
}); });
} }
TelemetryReporter.createActionEvent(BookTelemetryView, NbTelemetryActions.TrustNotebook).send(); TelemetryReporter.sendActionEvent(BookTelemetryView, NbTelemetryActions.TrustNotebook);
vscode.window.showInformationMessage(loc.msgBookTrusted); vscode.window.showInformationMessage(loc.msgBookTrusted);
} else { } else {
vscode.window.showInformationMessage(loc.msgBookAlreadyTrusted); vscode.window.showInformationMessage(loc.msgBookAlreadyTrusted);
@@ -134,7 +135,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
let bookPathToUpdate = bookTreeItem.book?.contentPath; let bookPathToUpdate = bookTreeItem.book?.contentPath;
if (bookPathToUpdate) { if (bookPathToUpdate) {
let pinStatusChanged = await this.bookPinManager.pinNotebook(bookTreeItem); let pinStatusChanged = await this.bookPinManager.pinNotebook(bookTreeItem);
TelemetryReporter.createActionEvent(BookTelemetryView, NbTelemetryActions.PinNotebook).send(); TelemetryReporter.sendActionEvent(BookTelemetryView, NbTelemetryActions.PinNotebook);
if (pinStatusChanged) { if (pinStatusChanged) {
bookTreeItem.contextValue = 'pinnedNotebook'; bookTreeItem.contextValue = 'pinnedNotebook';
} }
@@ -154,7 +155,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
async createBook(): Promise<void> { async createBook(): Promise<void> {
const dialog = new CreateBookDialog(this.bookTocManager); const dialog = new CreateBookDialog(this.bookTocManager);
dialog.createDialog(); dialog.createDialog();
TelemetryReporter.createActionEvent(BookTelemetryView, NbTelemetryActions.CreateBook).send(); TelemetryReporter.sendActionEvent(BookTelemetryView, NbTelemetryActions.CreateBook);
} }
async getSelectionQuickPick(movingElement: BookTreeItem): Promise<quickPickResults> { async getSelectionQuickPick(movingElement: BookTreeItem): Promise<quickPickResults> {
@@ -207,6 +208,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
} }
async editBook(movingElement: BookTreeItem): Promise<void> { async editBook(movingElement: BookTreeItem): Promise<void> {
TelemetryReporter.sendActionEvent(BookTelemetryView, NbTelemetryActions.MoveNotebook);
const selectionResults = await this.getSelectionQuickPick(movingElement); const selectionResults = await this.getSelectionQuickPick(movingElement);
if (selectionResults) { if (selectionResults) {
const pickedSection = selectionResults.quickPickSection; const pickedSection = selectionResults.quickPickSection;
@@ -238,7 +240,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
await this.showPreviewFile(urlToOpen); await this.showPreviewFile(urlToOpen);
} }
TelemetryReporter.createActionEvent(BookTelemetryView, NbTelemetryActions.OpenBook).send(); TelemetryReporter.sendActionEvent(BookTelemetryView, NbTelemetryActions.OpenBook);
} catch (e) { } catch (e) {
// if there is an error remove book from context // if there is an error remove book from context
const index = this.books.findIndex(book => book.bookPath === bookPath); const index = this.books.findIndex(book => book.bookPath === bookPath);
@@ -298,7 +300,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
} }
this._onDidChangeTreeData.fire(undefined); this._onDidChangeTreeData.fire(undefined);
} }
TelemetryReporter.createActionEvent(BookTelemetryView, NbTelemetryActions.CloseBook).send(); TelemetryReporter.sendActionEvent(BookTelemetryView, NbTelemetryActions.CloseBook);
} catch (e) { } catch (e) {
vscode.window.showErrorMessage(loc.closeBookError(book.root, e instanceof Error ? e.message : e)); vscode.window.showErrorMessage(loc.closeBookError(book.root, e instanceof Error ? e.message : e));
} finally { } finally {
@@ -383,7 +385,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
this._visitedNotebooks = this._visitedNotebooks.concat([normalizedResource]); this._visitedNotebooks = this._visitedNotebooks.concat([normalizedResource]);
} }
} }
TelemetryReporter.createActionEvent(BookTelemetryView, NbTelemetryActions.OpenNotebookFromBook); TelemetryReporter.sendActionEvent(BookTelemetryView, NbTelemetryActions.OpenNotebookFromBook);
} catch (e) { } catch (e) {
vscode.window.showErrorMessage(loc.openNotebookError(resource, e instanceof Error ? e.message : e)); vscode.window.showErrorMessage(loc.openNotebookError(resource, e instanceof Error ? e.message : e));
} }
@@ -396,10 +398,10 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
if (!uri) { if (!uri) {
let openDocument = azdata.nb.activeNotebookEditor; let openDocument = azdata.nb.activeNotebookEditor;
if (openDocument) { if (openDocument) {
notebookPath = openDocument.document.uri.fsPath.replace(/\\/g, '/'); notebookPath = openDocument.document.uri.fsPath;
} }
} else if (uri.fsPath) { } else if (uri.fsPath) {
notebookPath = uri.fsPath.replace(/\\/g, '/'); notebookPath = uri.fsPath;
} }
if (shouldReveal || this._bookViewer?.visible) { if (shouldReveal || this._bookViewer?.visible) {
@@ -414,6 +416,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
} }
async findAndExpandParentNode(notebookPath: string): Promise<BookTreeItem | undefined> { async findAndExpandParentNode(notebookPath: string): Promise<BookTreeItem | undefined> {
notebookPath = notebookPath.replace(/\\/g, '/');
const parentBook = this.books.find(b => notebookPath.indexOf(b.bookPath) > -1); const parentBook = this.books.find(b => notebookPath.indexOf(b.bookPath) > -1);
if (!parentBook) { if (!parentBook) {
// No parent book, likely because the Notebook is at the top level and not under a Notebook. // No parent book, likely because the Notebook is at the top level and not under a Notebook.
@@ -430,23 +433,23 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
// the top we'll expand nodes until we find the parent of the Notebook we're looking for // the top we'll expand nodes until we find the parent of the Notebook we're looking for
// get the children of root node and expand the nodes to the notebook level. // get the children of root node and expand the nodes to the notebook level.
await this.getChildren(parentBook.rootNode); await this.getChildren(parentBook.rootNode);
// The path to the parent of the Notebook we're looking for (this is the node we're looking to expand) // The path to the Notebook we're looking for (these are the nodes we're looking to expand)
const parentPath = notebookPath.substring(0, notebookPath.lastIndexOf(path.posix.sep)); const notebookFolders = notebookPath.split('/');
// Find number of directories between the Notebook path and the root of the book it's contained in // Find number of directories between the Notebook path and the root of the book it's contained in
// so we know how many parent nodes to expand // so we know how many parent nodes to expand
let depthOfNotebookInBook: number = path.relative(notebookPath, parentBook.bookPath).split(path.sep).length; let depthOfNotebookInBook: number = path.relative(notebookPath, parentBook.bookPath).split(path.sep).length;
// Walk the tree, expanding parent nodes as needed to load the child nodes until // Walk the tree, expanding parent nodes as needed to load the child nodes until
// we find the one for our Notebook // we find the one for our Notebook
while (depthOfNotebookInBook > 0) { while (depthOfNotebookInBook > -1) {
// check if the notebook is available in already expanded levels. // check if the notebook is available in already expanded levels.
bookItem = parentBook.bookItems.find(b => b.tooltip === notebookPath); bookItem = parentBook.bookItems.find(b => b.tooltip === notebookPath);
if (bookItem) { if (bookItem) {
return bookItem; return bookItem;
} }
// Search for the parent item // Walk down from the top level parent folder one level at each iteration
// notebook can be inside the same folder as parent and can be in a different folder as well // and keep expanding until we reach the target notebook leaf
// so check for both scenarios. let parentBookPath: string = notebookFolders.slice(0, notebookFolders.length - depthOfNotebookInBook).join('/');
let bookItemToExpand = parentBook.bookItems.find(b => b.tooltip.indexOf(parentPath) > -1) ?? let bookItemToExpand = parentBook.bookItems.find(b => b.tooltip.indexOf(parentBookPath) > -1) ??
parentBook.bookItems.find(b => path.relative(notebookPath, b.tooltip)?.split(path.sep)?.length === depthOfNotebookInBook); parentBook.bookItems.find(b => path.relative(notebookPath, b.tooltip)?.split(path.sep)?.length === depthOfNotebookInBook);
if (!bookItemToExpand) { if (!bookItemToExpand) {
break; break;
@@ -456,7 +459,13 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
// continue expanding and search its children // continue expanding and search its children
await this.getChildren(bookItemToExpand); await this.getChildren(bookItemToExpand);
} }
await this._bookViewer.reveal(bookItemToExpand, { select: false, focus: true, expand: 3 }); try {
// TO DO: Check why the reveal fails during initial load with 'TreeError [bookTreeView] Tree element not found'
await this._bookViewer.reveal(bookItemToExpand, { select: false, focus: true, expand: true });
}
catch (e) {
console.error(e);
}
depthOfNotebookInBook--; depthOfNotebookInBook--;
} }
return bookItem; return bookItem;

View File

@@ -29,7 +29,7 @@ export class BookTrustManager implements IBookTrustManager {
let hasTrustedBookPath: boolean = treeBookItems let hasTrustedBookPath: boolean = treeBookItems
.filter(bookItem => trustableBookPaths.some(trustableBookPath => trustableBookPath === path.join(bookItem.book.root, path.sep))) .filter(bookItem => trustableBookPaths.some(trustableBookPath => trustableBookPath === path.join(bookItem.book.root, path.sep)))
.some(bookItem => normalizedNotebookUri.startsWith(bookItem.book.version === BookVersion.v1 ? path.join(bookItem.book.root, 'content', path.sep) : path.join(bookItem.book.root, path.sep))); .some(bookItem => normalizedNotebookUri.startsWith(bookItem.book.version === BookVersion.v1 ? path.join(bookItem.book.root, 'content', path.sep) : path.join(bookItem.book.root, path.sep)));
let isNotebookTrusted = hasTrustedBookPath && this.books.some(bookModel => bookModel.getNotebook(normalizedNotebookUri)); let isNotebookTrusted = hasTrustedBookPath && this.books.some(bookModel => bookModel.getNotebook(vscode.Uri.file(normalizedNotebookUri).fsPath));
return isNotebookTrusted; return isNotebookTrusted;
} }

View File

@@ -19,6 +19,7 @@ export enum NbTelemetryActions {
SaveBook = 'BookSaved', SaveBook = 'BookSaved',
CreateBook = 'BookCreated', CreateBook = 'BookCreated',
PinNotebook = 'NotebookPinned', PinNotebook = 'NotebookPinned',
OpenNotebookFromBook = 'NotebookOpenedFromBook' OpenNotebookFromBook = 'NotebookOpenedFromBook',
MoveNotebook = 'MoveNotebook',
} }

View File

@@ -233,7 +233,7 @@ describe('BooksTreeViewTests', function () {
}); });
it.skip('getParent should return when element is a valid child notebook', async () => { it('getParent should return when element is a valid child notebook', async () => {
let parent = await bookTreeViewProvider.getParent(); let parent = await bookTreeViewProvider.getParent();
should(parent).be.undefined(); should(parent).be.undefined();
@@ -243,8 +243,9 @@ describe('BooksTreeViewTests', function () {
}); });
it('revealActiveDocumentInViewlet should return correct bookItem for highlight', async () => { it('revealActiveDocumentInViewlet should return correct bookItem for highlight', async () => {
let notebook1Path = vscode.Uri.file(path.join(rootFolderPath, 'Book', 'content', 'notebook1.ipynb')).fsPath; let notebook1Path = path.join(rootFolderPath, 'Book', 'content', 'notebook1.ipynb').replace(/\\/g, '/');
let currentSelection = await bookTreeViewProvider.findAndExpandParentNode(notebook1Path); let currentSelection = await bookTreeViewProvider.findAndExpandParentNode(notebook1Path);
should(currentSelection).not.be.undefined();
equalBookItems(currentSelection, expectedNotebook1); equalBookItems(currentSelection, expectedNotebook1);
}); });
@@ -327,8 +328,9 @@ describe('BooksTreeViewTests', function () {
}); });
it('revealActiveDocumentInViewlet should return correct bookItem for highlight', async () => { it('revealActiveDocumentInViewlet should return correct bookItem for highlight', async () => {
let notebook1Path = path.join(rootFolderPath, 'Book', 'content', 'notebook1.ipynb'); let notebook1Path = path.join(rootFolderPath, 'Book', 'content', 'notebook1.ipynb').replace(/\\/g, '/');
let currentSelection = await providedbookTreeViewProvider.findAndExpandParentNode(notebook1Path); let currentSelection = await providedbookTreeViewProvider.findAndExpandParentNode(notebook1Path);
should(currentSelection).not.be.undefined();
equalBookItems(currentSelection, expectedNotebook1); equalBookItems(currentSelection, expectedNotebook1);
}); });

View File

@@ -91,9 +91,14 @@ export class SchemaCompareMainWindow {
let sourceDacpac = context as string; let sourceDacpac = context as string;
if (profile) { if (profile) {
let ownerUri = await azdata.connection.getUriForConnection((profile.id)); let ownerUri = await azdata.connection.getUriForConnection((profile.id));
let usr = profile.userName;
if (!usr) {
usr = loc.defaultText;
}
this.sourceEndpointInfo = { this.sourceEndpointInfo = {
endpointType: mssql.SchemaCompareEndpointType.Database, endpointType: mssql.SchemaCompareEndpointType.Database,
serverDisplayName: `${profile.serverName} ${profile.userName}`, serverDisplayName: `${profile.serverName} (${usr})`,
serverName: profile.serverName, serverName: profile.serverName,
databaseName: profile.databaseName, databaseName: profile.databaseName,
ownerUri: ownerUri, ownerUri: ownerUri,

View File

@@ -2,12 +2,12 @@
"name": "sql-database-projects", "name": "sql-database-projects",
"displayName": "SQL Database Projects", "displayName": "SQL Database Projects",
"description": "The SQL Database Projects extension for Azure Data Studio allows users to develop and publish database schemas.", "description": "The SQL Database Projects extension for Azure Data Studio allows users to develop and publish database schemas.",
"version": "0.8.0", "version": "0.8.2",
"publisher": "Microsoft", "publisher": "Microsoft",
"preview": true, "preview": true,
"engines": { "engines": {
"vscode": "^1.30.1", "vscode": "^1.30.1",
"azdata": ">=1.27.0" "azdata": ">=1.28.0"
}, },
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
"icon": "images/sqlDatabaseProjects.png", "icon": "images/sqlDatabaseProjects.png",

View File

@@ -52,61 +52,65 @@ export class ProjectsController {
this.buildHelper = new BuildHelper(); this.buildHelper = new BuildHelper();
} }
public get dashboardDeployData(): (string | dataworkspace.IconCellValue)[][] { public getDashboardDeployData(projectFile: string): (string | dataworkspace.IconCellValue)[][] {
const infoRows: (string | dataworkspace.IconCellValue)[][] = []; const infoRows: (string | dataworkspace.IconCellValue)[][] = [];
let count = 0; let count = 0;
for (let i = this.deployInfo.length - 1; i >= 0; i--) { for (let i = this.deployInfo.length - 1; i >= 0; i--) {
let icon: azdata.IconPath; if (this.deployInfo[i].projectFile === projectFile) {
let text: string; let icon: azdata.IconPath;
if (this.deployInfo[i].status === Status.success) { let text: string;
icon = IconPathHelper.success; if (this.deployInfo[i].status === Status.success) {
text = constants.Success; icon = IconPathHelper.success;
} else if (this.deployInfo[i].status === Status.failed) { text = constants.Success;
icon = IconPathHelper.error; } else if (this.deployInfo[i].status === Status.failed) {
text = constants.Failed; icon = IconPathHelper.error;
} else { text = constants.Failed;
icon = IconPathHelper.inProgress; } else {
text = constants.InProgress; icon = IconPathHelper.inProgress;
} text = constants.InProgress;
}
let infoRow: (string | dataworkspace.IconCellValue)[] = [count.toString(), let infoRow: (string | dataworkspace.IconCellValue)[] = [count.toString(),
{ text: text, icon: icon }, { text: text, icon: icon },
this.deployInfo[i].target, this.deployInfo[i].target,
this.deployInfo[i].timeToCompleteAction, this.deployInfo[i].timeToCompleteAction,
this.deployInfo[i].startDate]; this.deployInfo[i].startDate];
infoRows.push(infoRow); infoRows.push(infoRow);
count++; count++;
}
} }
return infoRows; return infoRows;
} }
public get dashboardBuildData(): (string | dataworkspace.IconCellValue)[][] { public getDashboardBuildData(projectFile: string): (string | dataworkspace.IconCellValue)[][] {
const infoRows: (string | dataworkspace.IconCellValue)[][] = []; const infoRows: (string | dataworkspace.IconCellValue)[][] = [];
let count = 0; let count = 0;
for (let i = this.buildInfo.length - 1; i >= 0; i--) { for (let i = this.buildInfo.length - 1; i >= 0; i--) {
let icon: azdata.IconPath; if (this.buildInfo[i].projectFile === projectFile) {
let text: string; let icon: azdata.IconPath;
if (this.buildInfo[i].status === Status.success) { let text: string;
icon = IconPathHelper.success; if (this.buildInfo[i].status === Status.success) {
text = constants.Success; icon = IconPathHelper.success;
} else if (this.buildInfo[i].status === Status.failed) { text = constants.Success;
icon = IconPathHelper.error; } else if (this.buildInfo[i].status === Status.failed) {
text = constants.Failed; icon = IconPathHelper.error;
} else { text = constants.Failed;
icon = IconPathHelper.inProgress; } else {
text = constants.InProgress; icon = IconPathHelper.inProgress;
} text = constants.InProgress;
}
let infoRow: (string | dataworkspace.IconCellValue)[] = [count.toString(), let infoRow: (string | dataworkspace.IconCellValue)[] = [count.toString(),
{ text: text, icon: icon }, { text: text, icon: icon },
this.buildInfo[i].target, this.buildInfo[i].target,
this.buildInfo[i].timeToCompleteAction, this.buildInfo[i].timeToCompleteAction,
this.buildInfo[i].startDate]; this.buildInfo[i].startDate];
infoRows.push(infoRow); infoRows.push(infoRow);
count++; count++;
}
} }
return infoRows; return infoRows;
@@ -176,7 +180,7 @@ export class ProjectsController {
const startTime = new Date(); const startTime = new Date();
const currentBuildTimeInfo = `${startTime.toLocaleDateString()} ${constants.at} ${startTime.toLocaleTimeString()}`; const currentBuildTimeInfo = `${startTime.toLocaleDateString()} ${constants.at} ${startTime.toLocaleTimeString()}`;
let buildInfoNew = new DashboardData(Status.inProgress, project.getProjectTargetVersion(), currentBuildTimeInfo); let buildInfoNew = new DashboardData(project.projectFilePath, Status.inProgress, project.getProjectTargetVersion(), currentBuildTimeInfo);
this.buildInfo.push(buildInfoNew); this.buildInfo.push(buildInfoNew);
if (this.buildInfo.length - 1 === maxTableLength) { if (this.buildInfo.length - 1 === maxTableLength) {
@@ -276,7 +280,7 @@ export class ProjectsController {
const actionStartTime = currentDate.getTime(); const actionStartTime = currentDate.getTime();
const currentDeployTimeInfo = `${currentDate.toLocaleDateString()} ${constants.at} ${currentDate.toLocaleTimeString()}`; const currentDeployTimeInfo = `${currentDate.toLocaleDateString()} ${constants.at} ${currentDate.toLocaleTimeString()}`;
let deployInfoNew = new DashboardData(Status.inProgress, project.getProjectTargetVersion(), currentDeployTimeInfo); let deployInfoNew = new DashboardData(project.projectFilePath, Status.inProgress, project.getProjectTargetVersion(), currentDeployTimeInfo);
this.deployInfo.push(deployInfoNew); this.deployInfo.push(deployInfoNew);
if (this.deployInfo.length - 1 === maxTableLength) { if (this.deployInfo.length - 1 === maxTableLength) {
@@ -315,7 +319,7 @@ export class ProjectsController {
telemetryProps.totalDuration = (actionEndTime - buildStartTime).toString(); telemetryProps.totalDuration = (actionEndTime - buildStartTime).toString();
const currentDeployIndex = this.deployInfo.findIndex(d => d.startDate === currentDeployTimeInfo); const currentDeployIndex = this.deployInfo.findIndex(d => d.startDate === currentDeployTimeInfo);
this.deployInfo[currentDeployIndex].status = Status.success; this.deployInfo[currentDeployIndex].status = result.success ? Status.success : Status.failed;
this.deployInfo[currentDeployIndex].timeToCompleteAction = utils.timeConversion(timeToDeploy); this.deployInfo[currentDeployIndex].timeToCompleteAction = utils.timeConversion(timeToDeploy);
TelemetryReporter.createActionEvent(TelemetryViews.ProjectController, TelemetryActions.publishProject) TelemetryReporter.createActionEvent(TelemetryViews.ProjectController, TelemetryActions.publishProject)

View File

@@ -4,12 +4,14 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
export class DashboardData { export class DashboardData {
public projectFile: string;
public status: Status; public status: Status;
public target: string; public target: string;
public timeToCompleteAction: string; public timeToCompleteAction: string;
public startDate: string; public startDate: string;
constructor(status: Status, target: string, startDate: string) { constructor(projectFile: string, status: Status, target: string, startDate: string) {
this.projectFile = projectFile;
this.status = status; this.status = status;
this.target = target; this.target = target;
this.timeToCompleteAction = ''; this.timeToCompleteAction = '';

View File

@@ -128,7 +128,7 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide
/** /**
* Gets the data to be displayed in the project dashboard * Gets the data to be displayed in the project dashboard
*/ */
get dashboardComponents(): dataworkspace.IDashboardTable[] { getDashboardComponents(projectFile: string): dataworkspace.IDashboardTable[] {
const deployInfo: dataworkspace.IDashboardTable = { const deployInfo: dataworkspace.IDashboardTable = {
name: constants.Deployments, name: constants.Deployments,
columns: [{ displayName: constants.ID, width: 100 }, columns: [{ displayName: constants.ID, width: 100 },
@@ -136,7 +136,7 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide
{ displayName: constants.Target, width: 250 }, { displayName: constants.Target, width: 250 },
{ displayName: constants.Time, width: 250 }, { displayName: constants.Time, width: 250 },
{ displayName: constants.Date, width: 250 }], { displayName: constants.Date, width: 250 }],
data: this.projectController.dashboardDeployData data: this.projectController.getDashboardDeployData(projectFile)
}; };
const buildInfo: dataworkspace.IDashboardTable = { const buildInfo: dataworkspace.IDashboardTable = {
@@ -146,7 +146,7 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide
{ displayName: constants.Target, width: 250 }, { displayName: constants.Target, width: 250 },
{ displayName: constants.Time, width: 250 }, { displayName: constants.Time, width: 250 },
{ displayName: constants.Date, width: 250 }], { displayName: constants.Date, width: 250 }],
data: this.projectController.dashboardBuildData data: this.projectController.getDashboardBuildData(projectFile)
}; };
return [deployInfo, buildInfo]; return [deployInfo, buildInfo];

View File

@@ -1,7 +1,7 @@
{ {
"name": "azuredatastudio", "name": "azuredatastudio",
"version": "1.28.0", "version": "1.28.0",
"distro": "4462480cd081b5729600b15921dbb445b46b0de9", "distro": "1d8bd1032738ec5b6aad0b80551eee7376a617dc",
"author": { "author": {
"name": "Microsoft Corporation" "name": "Microsoft Corporation"
}, },

View File

@@ -81,7 +81,7 @@
"builtInExtensions": [ "builtInExtensions": [
{ {
"name": "Microsoft.sqlservernotebook", "name": "Microsoft.sqlservernotebook",
"version": "0.3.9", "version": "0.4.0",
"repo": "https://github.com/Microsoft/azuredatastudio" "repo": "https://github.com/Microsoft/azuredatastudio"
} }
], ],

View File

@@ -37,7 +37,7 @@ export class HeaderFilter<T extends Slick.SlickData> {
private columnDef!: FilterableColumn<T>; private columnDef!: FilterableColumn<T>;
private buttonStyles?: IButtonStyles; private buttonStyles?: IButtonStyles;
private disposableStore = new DisposableStore(); private disposableStore = new DisposableStore();
public enabled: boolean = true; private _enabled: boolean = true;
constructor() { constructor() {
} }
@@ -435,4 +435,18 @@ export class HeaderFilter<T extends Slick.SlickData> {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
} }
public get enabled(): boolean {
return this._enabled;
}
public set enabled(value: boolean) {
if (this._enabled !== value) {
this._enabled = value;
// force the table header to redraw.
this.grid.getColumns().forEach((column) => {
this.grid.updateColumnHeader(column.id);
});
}
}
} }

View File

@@ -48,7 +48,7 @@ class TelemetryEventImpl implements ITelemetryEvent {
assign(this._properties, assign(this._properties,
{ {
authenticationType: connectionInfo?.authenticationType, authenticationType: connectionInfo?.authenticationType,
providerName: connectionInfo?.providerName provider: connectionInfo?.providerName
}); });
return this; return this;
} }

View File

@@ -75,12 +75,17 @@ export enum TelemetryAction {
RunQueryString = 'RunQueryString', RunQueryString = 'RunQueryString',
ShowChart = 'ShowChart', ShowChart = 'ShowChart',
StopAgentJob = 'StopAgentJob', StopAgentJob = 'StopAgentJob',
WizardPagesNavigation = 'WizardPagesNavigation' WizardPagesNavigation = 'WizardPagesNavigation',
SearchStarted = 'SearchStarted',
SearchCompleted = 'SearchCompleted'
} }
export enum NbTelemetryAction { export enum NbTelemetryAction {
RunCell = 'RunCell', RunCell = 'RunCell',
RunAll = 'RunNotebook' RunAll = 'RunNotebook',
AddCell = 'AddCell',
KernelChanged = 'KernelChanged',
NewNotebookFromConnections = 'NewNotebookWithConnectionProfile'
} }
export enum TelemetryPropertyName { export enum TelemetryPropertyName {

View File

@@ -19,6 +19,9 @@ import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilit
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
export const BackupFeatureName = 'backup'; export const BackupFeatureName = 'backup';
export const backupIsPreviewFeature = localize('backup.isPreviewFeature', "You must enable preview features in order to use backup");
export const backupNotSupportedOutOfDBContext = localize('backup.commandNotSupportedForServer', "Backup command is not supported outside of a database context. Please select a database and try again.");
export const backupNotSupportedForAzure = localize('backup.commandNotSupported', "Backup command is not supported for Azure SQL databases.");
export function showBackup(accessor: ServicesAccessor, connection: IConnectionProfile): Promise<void> { export function showBackup(accessor: ServicesAccessor, connection: IConnectionProfile): Promise<void> {
const backupUiService = accessor.get(IBackupUiService); const backupUiService = accessor.get(IBackupUiService);
@@ -43,7 +46,7 @@ export class BackupAction extends Task {
const configurationService = accessor.get<IConfigurationService>(IConfigurationService); const configurationService = accessor.get<IConfigurationService>(IConfigurationService);
const previewFeaturesEnabled = configurationService.getValue<{ enablePreviewFeatures: boolean }>('workbench').enablePreviewFeatures; const previewFeaturesEnabled = configurationService.getValue<{ enablePreviewFeatures: boolean }>('workbench').enablePreviewFeatures;
if (!previewFeaturesEnabled) { if (!previewFeaturesEnabled) {
return accessor.get<INotificationService>(INotificationService).info(localize('backup.isPreviewFeature', "You must enable preview features in order to use backup")); return accessor.get<INotificationService>(INotificationService).info(backupIsPreviewFeature);
} }
const connectionManagementService = accessor.get<IConnectionManagementService>(IConnectionManagementService); const connectionManagementService = accessor.get<IConnectionManagementService>(IConnectionManagementService);
@@ -55,17 +58,20 @@ export class BackupAction extends Task {
if (profile) { if (profile) {
const serverInfo = connectionManagementService.getServerInfo(profile.id); const serverInfo = connectionManagementService.getServerInfo(profile.id);
if (serverInfo && serverInfo.isCloud && profile.providerName === mssqlProviderName) { if (serverInfo && serverInfo.isCloud && profile.providerName === mssqlProviderName) {
return accessor.get<INotificationService>(INotificationService).info(localize('backup.commandNotSupported', "Backup command is not supported for Azure SQL databases.")); return accessor.get<INotificationService>(INotificationService).info(backupNotSupportedForAzure);
} }
if (!profile.databaseName && profile.providerName === mssqlProviderName) { if (!profile.databaseName && profile.providerName === mssqlProviderName) {
return accessor.get<INotificationService>(INotificationService).info(localize('backup.commandNotSupportedForServer', "Backup command is not supported in Server Context. Please select a Database and try again.")); return accessor.get<INotificationService>(INotificationService).info(backupNotSupportedOutOfDBContext);
} }
} }
const capabilitiesService = accessor.get(ICapabilitiesService); const capabilitiesService = accessor.get(ICapabilitiesService);
const instantiationService = accessor.get(IInstantiationService); const instantiationService = accessor.get(IInstantiationService);
profile = profile ? profile : new ConnectionProfile(capabilitiesService, profile); profile = profile ? profile : new ConnectionProfile(capabilitiesService, profile);
if (!profile.databaseName) {
return accessor.get<INotificationService>(INotificationService).info(backupNotSupportedOutOfDBContext);
}
return instantiationService.invokeFunction(showBackup, profile); return instantiationService.invokeFunction(showBackup, profile);
} }
} }

View File

@@ -60,7 +60,7 @@ export class EditorReplacementContribution implements IWorkbenchContribution {
if (!language) { if (!language) {
// Attempt to use extension or extension of modified input (if in diff editor) // Attempt to use extension or extension of modified input (if in diff editor)
// remove the . // remove the .
language = editor instanceof DiffEditorInput ? path.extname(editor.modifiedInput.resource.toString()).slice(1) : path.extname(editor.resource.toString()).slice(1); language = editor instanceof DiffEditorInput ? path.extname(editor.modifiedInput.resource.fsPath).slice(1) : path.extname(editor.resource.toString()).slice(1);
} }
if (!language) { if (!language) {

View File

@@ -128,8 +128,10 @@ export default class WebViewComponent extends ComponentBase<WebViewProperties> i
if (!link) { if (!link) {
return; return;
} }
if (WebViewComponent.standardSupportedLinkSchemes.indexOf(link.scheme) >= 0 || this.enableCommandUris && link.scheme === 'command') { if (WebViewComponent.standardSupportedLinkSchemes.indexOf(link.scheme) >= 0) {
this._openerService.open(link); this._openerService.open(link);
} else if (this.enableCommandUris && link.scheme === 'command') {
this._openerService.open(link, { allowCommands: true });
} }
} }

View File

@@ -45,6 +45,7 @@ export class LinkCalloutDialog extends Modal {
dialogPosition: DialogPosition, dialogPosition: DialogPosition,
dialogProperties: IDialogProperties, dialogProperties: IDialogProperties,
private readonly _defaultLabel: string = '', private readonly _defaultLabel: string = '',
private readonly _defaultLinkUrl: string = '',
@IContextViewService private readonly _contextViewService: IContextViewService, @IContextViewService private readonly _contextViewService: IContextViewService,
@IThemeService themeService: IThemeService, @IThemeService themeService: IThemeService,
@ILayoutService layoutService: ILayoutService, @ILayoutService layoutService: ILayoutService,
@@ -134,6 +135,7 @@ export class LinkCalloutDialog extends Modal {
placeholder: constants.linkAddressPlaceholder, placeholder: constants.linkAddressPlaceholder,
ariaLabel: constants.linkAddressLabel ariaLabel: constants.linkAddressLabel
}); });
this._linkUrlInputBox.value = this._defaultLinkUrl;
DOM.append(linkAddressRow, linkAddressInputContainer); DOM.append(linkAddressRow, linkAddressInputContainer);
} }

View File

@@ -80,7 +80,7 @@ export class LinkHandlerDirective {
this.openerService.open(uri, { openExternal: true }).catch(onUnexpectedError); this.openerService.open(uri, { openExternal: true }).catch(onUnexpectedError);
} }
else { else {
this.openerService.open(uri).catch(onUnexpectedError); this.openerService.open(uri, { allowCommands: true }).catch(onUnexpectedError);
} }
} }
} }

View File

@@ -236,11 +236,14 @@ export class MarkdownToolbarComponent extends AngularDisposable {
needsTransform = false; needsTransform = false;
} else { } else {
let linkUrl = linkCalloutResult.insertUnescapedLinkUrl; let linkUrl = linkCalloutResult.insertUnescapedLinkUrl;
const isFile = URI.parse(linkUrl).scheme === 'file'; const isAnchorLink = linkUrl.startsWith('#');
if (isFile && !path.isAbsolute(linkUrl)) { if (!isAnchorLink) {
const notebookDirName = path.dirname(this.cellModel?.notebookModel?.notebookUri.fsPath); const isFile = URI.parse(linkUrl).scheme === 'file';
const relativePath = (linkUrl).replace(/\\/g, path.posix.sep); if (isFile && !path.isAbsolute(linkUrl)) {
linkUrl = path.resolve(notebookDirName, relativePath); const notebookDirName = path.dirname(this.cellModel?.notebookModel?.notebookUri.fsPath);
const relativePath = (linkUrl).replace(/\\/g, path.posix.sep);
linkUrl = path.resolve(notebookDirName, relativePath);
}
} }
// Otherwise, re-focus on the output element, and insert the link directly. // Otherwise, re-focus on the output element, and insert the link directly.
this.output?.nativeElement?.focus(); this.output?.nativeElement?.focus();
@@ -296,7 +299,8 @@ export class MarkdownToolbarComponent extends AngularDisposable {
if (type === MarkdownButtonType.LINK_PREVIEW) { if (type === MarkdownButtonType.LINK_PREVIEW) {
const defaultLabel = this.getCurrentSelectionText(); const defaultLabel = this.getCurrentSelectionText();
this._linkCallout = this._instantiationService.createInstance(LinkCalloutDialog, this.insertLinkHeading, dialogPosition, dialogProperties, defaultLabel); const defaultLinkUrl = this.getCurrentLinkUrl();
this._linkCallout = this._instantiationService.createInstance(LinkCalloutDialog, this.insertLinkHeading, dialogPosition, dialogProperties, defaultLabel, defaultLinkUrl);
this._linkCallout.render(); this._linkCallout.render();
calloutOptions = await this._linkCallout.open(); calloutOptions = await this._linkCallout.open();
} }
@@ -318,6 +322,14 @@ export class MarkdownToolbarComponent extends AngularDisposable {
} }
} }
private getCurrentLinkUrl(): string {
if (document.getSelection().anchorNode.parentNode['protocol'] === 'file:') {
return document.getSelection().anchorNode.parentNode['pathname'] || '';
} else {
return document.getSelection().anchorNode.parentNode['href'] || '';
}
}
private getCellEditorControl(): IEditor | undefined { private getCellEditorControl(): IEditor | undefined {
// If control doesn't exist, editor may have been destroyed previously when switching edit modes // If control doesn't exist, editor may have been destroyed previously when switching edit modes
if (!this._cellEditor?.getEditor()?.getControl()) { if (!this._cellEditor?.getEditor()?.getControl()) {

View File

@@ -132,15 +132,28 @@ export class HTMLMarkdownConverter {
this.turndownService.addRule('a', { this.turndownService.addRule('a', {
filter: 'a', filter: 'a',
replacement: (content, node) => { replacement: (content, node) => {
//On Windows, if notebook is not trusted then the href attr is removed for all non-web URL links let href = node.href;
// href contains either a hyperlink or a URI-encoded absolute path. (See resolveUrls method in notebookMarkdown.ts) let notebookLink: URI | undefined;
const notebookLink = node.href ? URI.parse(node.href) : URI.file(node.title); const isAnchorLinkInFile = (node.attributes.href?.nodeValue.startsWith('#') || href.includes('#')) && href.startsWith('file://');
const notebookFolder = this.notebookUri ? path.join(path.dirname(this.notebookUri.fsPath), path.sep) : ''; if (isAnchorLinkInFile) {
let relativePath = findPathRelativeToContent(notebookFolder, notebookLink); notebookLink = getUriAnchorLink(node, this.notebookUri);
if (relativePath) { } else {
return `[${node.innerText}](${relativePath})`; //On Windows, if notebook is not trusted then the href attr is removed for all non-web URL links
// href contains either a hyperlink or a URI-encoded absolute path. (See resolveUrls method in notebookMarkdown.ts)
notebookLink = href ? URI.parse(href) : URI.file(node.title);
} }
return `[${content}](${node.href})`; const notebookFolder = this.notebookUri ? path.join(path.dirname(this.notebookUri.fsPath), path.sep) : '';
if (notebookLink.fsPath !== this.notebookUri.fsPath) {
let relativePath = findPathRelativeToContent(notebookFolder, notebookLink);
if (relativePath) {
return `[${node.innerText}](${relativePath})`;
}
} else if (notebookLink?.fragment) {
// if the anchor link is to a section in the same notebook then just add the fragment
return `[${content}](${notebookLink.fragment})`;
}
return `[${content}](${href})`;
} }
}); });
// Only nested list case differs from original turndown rule // Only nested list case differs from original turndown rule
@@ -275,7 +288,7 @@ function blankReplacement(content, node) {
export function findPathRelativeToContent(notebookFolder: string, contentPath: URI | undefined): string { export function findPathRelativeToContent(notebookFolder: string, contentPath: URI | undefined): string {
if (notebookFolder) { if (notebookFolder) {
if (contentPath?.scheme === 'file') { if (contentPath?.scheme === 'file') {
let relativePath = path.relative(notebookFolder, contentPath.fsPath); let relativePath = contentPath.fragment ? path.relative(notebookFolder, contentPath.fsPath).concat('#', contentPath.fragment) : path.relative(notebookFolder, contentPath.fsPath);
//if path contains whitespaces then it's not identified as a link //if path contains whitespaces then it's not identified as a link
relativePath = relativePath.replace(/\s/g, '%20'); relativePath = relativePath.replace(/\s/g, '%20');
if (relativePath.startsWith(path.join('..', path.sep) || path.join('.', path.sep))) { if (relativePath.startsWith(path.join('..', path.sep) || path.join('.', path.sep))) {
@@ -295,3 +308,15 @@ export function addHighlightIfYellowBgExists(node, content: string): string {
} }
return content; return content;
} }
export function getUriAnchorLink(node, notebookUri: URI): URI {
const sectionLinkToAnotherFile = node.href.includes('#') && !node.attributes.href?.nodeValue.startsWith('#');
if (sectionLinkToAnotherFile) {
let absolutePath = !path.isAbsolute(node.attributes.href?.nodeValue) ? path.resolve(path.dirname(notebookUri.fsPath), node.attributes.href?.nodeValue) : node.attributes.href?.nodeValue;
// if section link is different from the current notebook
return URI.file(absolutePath);
} else {
// else build an uri using the current notebookUri
return URI.from({ scheme: 'file', path: notebookUri.path, fragment: node.attributes.href?.nodeValue });
}
}

View File

@@ -28,6 +28,8 @@ import { INotebookService } from 'sql/workbench/services/notebook/browser/notebo
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { CellContext } from 'sql/workbench/contrib/notebook/browser/cellViews/codeActions'; import { CellContext } from 'sql/workbench/contrib/notebook/browser/cellViews/codeActions';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
const msgLoading = localize('loading', "Loading kernels..."); const msgLoading = localize('loading', "Loading kernels...");
export const msgChanging = localize('changing', "Changing kernel..."); export const msgChanging = localize('changing', "Changing kernel...");
@@ -46,7 +48,8 @@ export class AddCellAction extends Action {
constructor( constructor(
id: string, label: string, cssClass: string, id: string, label: string, cssClass: string,
@INotebookService private _notebookService: INotebookService @INotebookService private _notebookService: INotebookService,
@IAdsTelemetryService private _telemetryService: IAdsTelemetryService,
) { ) {
super(id, label, cssClass); super(id, label, cssClass);
} }
@@ -68,6 +71,9 @@ export class AddCellAction extends Action {
const index = editor.cells?.findIndex(cell => cell.active) ?? 0; const index = editor.cells?.findIndex(cell => cell.active) ?? 0;
editor.addCell(this.cellType, index); editor.addCell(this.cellType, index);
} }
this._telemetryService.createActionEvent(TelemetryKeys.TelemetryView.Notebook, TelemetryKeys.NbTelemetryAction.AddCell)
.withAdditionalProperties({ cell_type: this.cellType })
.send();
} }
} }
@@ -213,12 +219,14 @@ export class RunAllCellsAction extends Action {
constructor( constructor(
id: string, label: string, cssClass: string, id: string, label: string, cssClass: string,
@INotificationService private notificationService: INotificationService, @INotificationService private notificationService: INotificationService,
@INotebookService private _notebookService: INotebookService @INotebookService private _notebookService: INotebookService,
@IAdsTelemetryService private _telemetryService: IAdsTelemetryService,
) { ) {
super(id, label, cssClass); super(id, label, cssClass);
} }
public async run(context: URI): Promise<boolean> { public async run(context: URI): Promise<boolean> {
try { try {
this._telemetryService.sendActionEvent(TelemetryKeys.TelemetryView.Notebook, TelemetryKeys.NbTelemetryAction.RunAll);
const editor = this._notebookService.findNotebookEditor(context); const editor = this._notebookService.findNotebookEditor(context);
await editor.runAllCells(); await editor.runAllCells();
return true; return true;
@@ -280,7 +288,8 @@ const kernelDropdownElementId = 'kernel-dropdown';
export class KernelsDropdown extends SelectBox { export class KernelsDropdown extends SelectBox {
private model: NotebookModel; private model: NotebookModel;
private _showAllKernels: boolean = false; private _showAllKernels: boolean = false;
constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, modelReady: Promise<INotebookModel>, @IConfigurationService private _configurationService: IConfigurationService) { constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, modelReady: Promise<INotebookModel>, @IConfigurationService private _configurationService: IConfigurationService,
) {
super([msgLoading], msgLoading, contextViewProvider, container, { labelText: kernelLabel, labelOnTop: false, ariaLabel: kernelLabel, id: kernelDropdownElementId } as ISelectBoxOptionsWithLabel); super([msgLoading], msgLoading, contextViewProvider, container, { labelText: kernelLabel, labelOnTop: false, ariaLabel: kernelLabel, id: kernelDropdownElementId } as ISelectBoxOptionsWithLabel);
if (modelReady) { if (modelReady) {
@@ -549,13 +558,17 @@ export class NewNotebookAction extends Action {
id: string, id: string,
label: string, label: string,
@ICommandService private commandService: ICommandService, @ICommandService private commandService: ICommandService,
@IObjectExplorerService private objectExplorerService: IObjectExplorerService @IObjectExplorerService private objectExplorerService: IObjectExplorerService,
@IAdsTelemetryService private _telemetryService: IAdsTelemetryService,
) { ) {
super(id, label); super(id, label);
this.class = 'notebook-action new-notebook'; this.class = 'notebook-action new-notebook';
} }
async run(context?: azdata.ObjectExplorerContext): Promise<void> { async run(context?: azdata.ObjectExplorerContext): Promise<void> {
this._telemetryService.createActionEvent(TelemetryKeys.TelemetryView.Notebook, TelemetryKeys.NbTelemetryAction.NewNotebookFromConnections)
.withConnectionInfo(context?.connectionProfile)
.send();
let connProfile: azdata.IConnectionProfile; let connProfile: azdata.IConnectionProfile;
if (context && context.nodeInfo) { if (context && context.nodeInfo) {
let node = await this.objectExplorerService.getTreeNode(context.connectionProfile.id, context.nodeInfo.nodePath); let node = await this.objectExplorerService.getTreeNode(context.connectionProfile.id, context.nodeInfo.nodePath);

View File

@@ -39,6 +39,8 @@ import { NotebookSearchView } from 'sql/workbench/contrib/notebook/browser/noteb
import * as path from 'vs/base/common/path'; import * as path from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; import { TreeViewPane } from 'vs/workbench/browser/parts/views/treeView';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
export const VIEWLET_ID = 'workbench.view.notebooks'; export const VIEWLET_ID = 'workbench.view.notebooks';
@@ -124,7 +126,8 @@ export class NotebookExplorerViewPaneContainer extends ViewPaneContainer {
@IMenuService private menuService: IMenuService, @IMenuService private menuService: IMenuService,
@IContextKeyService private contextKeyService: IContextKeyService, @IContextKeyService private contextKeyService: IContextKeyService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IFileService private readonly fileService: IFileService @IFileService private readonly fileService: IFileService,
@IAdsTelemetryService private _telemetryService: IAdsTelemetryService
) { ) {
super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService);
this.inputBoxFocused = Constants.InputBoxFocusedKey.bindTo(this.contextKeyService); this.inputBoxFocused = Constants.InputBoxFocusedKey.bindTo(this.contextKeyService);
@@ -255,6 +258,9 @@ export class NotebookExplorerViewPaneContainer extends ViewPaneContainer {
onQueryValidationError(err); onQueryValidationError(err);
return; return;
} }
this._telemetryService.createActionEvent(TelemetryKeys.TelemetryView.Notebook, TelemetryKeys.TelemetryAction.SearchStarted)
.withAdditionalProperties({ triggeredOnType: triggeredOnType })
.send();
this.validateQuery(query).then(() => { this.validateQuery(query).then(() => {
if (this.views.length > 1) { if (this.views.length > 1) {

View File

@@ -43,6 +43,8 @@ import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchStop
import { Action, IAction } from 'vs/base/common/actions'; import { Action, IAction } from 'vs/base/common/actions';
import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { Memento } from 'vs/workbench/common/memento'; import { Memento } from 'vs/workbench/common/memento';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
const $ = dom.$; const $ = dom.$;
@@ -82,6 +84,7 @@ export class NotebookSearchView extends SearchView {
@IOpenerService openerService: IOpenerService, @IOpenerService openerService: IOpenerService,
@ITelemetryService telemetryService: ITelemetryService, @ITelemetryService telemetryService: ITelemetryService,
@ICommandService readonly commandService: ICommandService, @ICommandService readonly commandService: ICommandService,
@IAdsTelemetryService private _telemetryService: IAdsTelemetryService,
) { ) {
super(options, fileService, editorService, progressService, notificationService, dialogService, contextViewService, instantiationService, viewDescriptorService, configurationService, contextService, searchWorkbenchService, contextKeyService, replaceService, textFileService, preferencesService, themeService, searchHistoryService, contextMenuService, menuService, accessibilityService, keybindingService, storageService, openerService, telemetryService); super(options, fileService, editorService, progressService, notificationService, dialogService, contextViewService, instantiationService, viewDescriptorService, configurationService, contextService, searchWorkbenchService, contextKeyService, replaceService, textFileService, preferencesService, themeService, searchHistoryService, contextMenuService, menuService, accessibilityService, keybindingService, storageService, openerService, telemetryService);
@@ -239,6 +242,7 @@ export class NotebookSearchView extends SearchView {
} }
public startSearch(query: ITextQuery, excludePatternText: string, includePatternText: string, triggeredOnType: boolean, searchWidget: NotebookSearchWidget): Thenable<void> { public startSearch(query: ITextQuery, excludePatternText: string, includePatternText: string, triggeredOnType: boolean, searchWidget: NotebookSearchWidget): Thenable<void> {
let start = new Date().getTime();
let progressComplete: () => void; let progressComplete: () => void;
this.progressService.withProgress({ location: this.getProgressLocation(), delay: triggeredOnType ? 300 : 0 }, _progress => { this.progressService.withProgress({ location: this.getProgressLocation(), delay: triggeredOnType ? 300 : 0 }, _progress => {
return new Promise<void>(resolve => progressComplete = resolve); return new Promise<void>(resolve => progressComplete = resolve);
@@ -253,6 +257,12 @@ export class NotebookSearchView extends SearchView {
}, 2000); }, 2000);
const onComplete = async (completed?: ISearchComplete) => { const onComplete = async (completed?: ISearchComplete) => {
let end = new Date().getTime();
this._telemetryService.createActionEvent(TelemetryKeys.TelemetryView.Notebook, TelemetryKeys.TelemetryAction.SearchCompleted)
.withAdditionalProperties({ resultsReturned: completed?.results.length })
.withAdditionalMeasurements({ timeTakenMs: end - start })
.send();
clearTimeout(slowTimer); clearTimeout(slowTimer);
this.state = SearchUIState.Idle; this.state = SearchUIState.Idle;

View File

@@ -143,6 +143,12 @@ suite('HTML Markdown Converter', function (): void {
assert.equal(htmlMarkdownConverter.convert(htmlString), '[msft](http://www.microsoft.com/images/msft.png)', 'Basic http link test failed'); assert.equal(htmlMarkdownConverter.convert(htmlString), '[msft](http://www.microsoft.com/images/msft.png)', 'Basic http link test failed');
htmlString = 'Test <a href="http://www.microsoft.com/images/msft.png">msft</a>'; htmlString = 'Test <a href="http://www.microsoft.com/images/msft.png">msft</a>';
assert.equal(htmlMarkdownConverter.convert(htmlString), 'Test [msft](http://www.microsoft.com/images/msft.png)', 'Basic http link + text test failed'); assert.equal(htmlMarkdownConverter.convert(htmlString), 'Test [msft](http://www.microsoft.com/images/msft.png)', 'Basic http link + text test failed');
htmlString = '<a href="#hello">hello</a>';
assert.equal(htmlMarkdownConverter.convert(htmlString), '[hello](#hello)', 'Basic link to a section failed');
htmlString = '<a href="file.md#hello">hello</a>';
assert.equal(htmlMarkdownConverter.convert(htmlString), `[hello](.${path.sep}file.md#hello)`, 'Basic anchor link to a section failed');
htmlString = '<a href="http://www.microsoft.com/images/msft.png#Hello">hello</a>';
assert.equal(htmlMarkdownConverter.convert(htmlString), '[hello](http://www.microsoft.com/images/msft.png#Hello)', 'Http link containing # sign failed');
}); });
test('Should transform <li> tags', () => { test('Should transform <li> tags', () => {

View File

@@ -24,6 +24,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { NullAdsTelemetryService } from 'sql/platform/telemetry/common/adsTelemetryService';
class TestClientSession extends ClientSessionStub { class TestClientSession extends ClientSessionStub {
private _errorState: boolean = false; private _errorState: boolean = false;
@@ -124,7 +125,7 @@ suite('Notebook Actions', function (): void {
let actualCellType: CellType; let actualCellType: CellType;
let action = new AddCellAction('TestId', 'TestLabel', 'TestClass', mockNotebookService.object); let action = new AddCellAction('TestId', 'TestLabel', 'TestClass', mockNotebookService.object, new NullAdsTelemetryService());
action.cellType = testCellType; action.cellType = testCellType;
// Normal use case // Normal use case
@@ -191,7 +192,7 @@ suite('Notebook Actions', function (): void {
let mockNotification = TypeMoq.Mock.ofType<INotificationService>(TestNotificationService); let mockNotification = TypeMoq.Mock.ofType<INotificationService>(TestNotificationService);
mockNotification.setup(n => n.notify(TypeMoq.It.isAny())); mockNotification.setup(n => n.notify(TypeMoq.It.isAny()));
let action = new RunAllCellsAction('TestId', 'TestLabel', 'TestClass', mockNotification.object, mockNotebookService.object); let action = new RunAllCellsAction('TestId', 'TestLabel', 'TestClass', mockNotification.object, mockNotebookService.object, new NullAdsTelemetryService());
// Normal use case // Normal use case
mockNotebookEditor.setup(c => c.runAllCells()).returns(() => Promise.resolve(true)); mockNotebookEditor.setup(c => c.runAllCells()).returns(() => Promise.resolve(true));
@@ -251,7 +252,7 @@ suite('Notebook Actions', function (): void {
return Promise.resolve(true); return Promise.resolve(true);
}); });
let action = new NewNotebookAction('TestId', 'TestLabel', mockCommandService.object, undefined); let action = new NewNotebookAction('TestId', 'TestLabel', mockCommandService.object, undefined, new NullAdsTelemetryService());
action.run(undefined); action.run(undefined);
assert.strictEqual(actualCmdId, NewNotebookAction.INTERNAL_NEW_NOTEBOOK_CMD_ID); assert.strictEqual(actualCmdId, NewNotebookAction.INTERNAL_NEW_NOTEBOOK_CMD_ID);

View File

@@ -29,7 +29,7 @@ suite('Link Callout Dialog', function (): void {
}); });
test('Should return empty markdown on cancel', async function (): Promise<void> { test('Should return empty markdown on cancel', async function (): Promise<void> {
let linkCalloutDialog = new LinkCalloutDialog('Title', 'below', defaultDialogProperties, 'defaultLabel', let linkCalloutDialog = new LinkCalloutDialog('Title', 'below', defaultDialogProperties, 'defaultLabel', 'defaultLinkLabel',
undefined, themeService, layoutService, telemetryService, contextKeyService, undefined, undefined, undefined); undefined, themeService, layoutService, telemetryService, contextKeyService, undefined, undefined, undefined);
linkCalloutDialog.render(); linkCalloutDialog.render();
@@ -50,7 +50,7 @@ suite('Link Callout Dialog', function (): void {
test('Should return expected values on insert', async function (): Promise<void> { test('Should return expected values on insert', async function (): Promise<void> {
const defaultLabel = 'defaultLabel'; const defaultLabel = 'defaultLabel';
const sampleUrl = 'https://www.aka.ms/azuredatastudio'; const sampleUrl = 'https://www.aka.ms/azuredatastudio';
let linkCalloutDialog = new LinkCalloutDialog('Title', 'below', defaultDialogProperties, defaultLabel, let linkCalloutDialog = new LinkCalloutDialog('Title', 'below', defaultDialogProperties, defaultLabel, sampleUrl,
undefined, themeService, layoutService, telemetryService, contextKeyService, undefined, undefined, undefined); undefined, themeService, layoutService, telemetryService, contextKeyService, undefined, undefined, undefined);
linkCalloutDialog.render(); linkCalloutDialog.render();
@@ -73,7 +73,7 @@ suite('Link Callout Dialog', function (): void {
test('Should return expected values on insert when escape necessary', async function (): Promise<void> { test('Should return expected values on insert when escape necessary', async function (): Promise<void> {
const defaultLabel = 'default[]Label'; const defaultLabel = 'default[]Label';
const sampleUrl = 'https://www.aka.ms/azuredatastudio()'; const sampleUrl = 'https://www.aka.ms/azuredatastudio()';
let linkCalloutDialog = new LinkCalloutDialog('Title', 'below', defaultDialogProperties, defaultLabel, let linkCalloutDialog = new LinkCalloutDialog('Title', 'below', defaultDialogProperties, defaultLabel, sampleUrl,
undefined, themeService, layoutService, telemetryService, contextKeyService, undefined, undefined, undefined); undefined, themeService, layoutService, telemetryService, contextKeyService, undefined, undefined, undefined);
linkCalloutDialog.render(); linkCalloutDialog.render();
@@ -106,4 +106,27 @@ suite('Link Callout Dialog', function (): void {
assert.equal(escapeUrl('<>&()'), '&lt;&gt;&amp;%28%29', 'URL test known escaped characters failed'); assert.equal(escapeUrl('<>&()'), '&lt;&gt;&amp;%28%29', 'URL test known escaped characters failed');
assert.equal(escapeUrl('<>&()[]'), '&lt;&gt;&amp;%28%29[]', 'URL test all escaped characters failed'); assert.equal(escapeUrl('<>&()[]'), '&lt;&gt;&amp;%28%29[]', 'URL test all escaped characters failed');
}); });
test('Should return file link properly', async function (): Promise<void> {
const defaultLabel = 'defaultLabel';
const sampleUrl = 'C:/Test/Test.ipynb';
let linkCalloutDialog = new LinkCalloutDialog('Title', 'below', defaultDialogProperties, defaultLabel, sampleUrl,
undefined, themeService, layoutService, telemetryService, contextKeyService, undefined, undefined, undefined);
linkCalloutDialog.render();
let deferred = new Deferred<ILinkCalloutDialogOptions>();
// When I first open the callout dialog
linkCalloutDialog.open().then(value => {
deferred.resolve(value);
});
linkCalloutDialog.url = sampleUrl;
// And insert the dialog
linkCalloutDialog.insert();
let result = await deferred.promise;
assert.equal(result.insertUnescapedLinkLabel, defaultLabel, 'Label not returned correctly');
assert.equal(result.insertUnescapedLinkUrl, sampleUrl, 'URL not returned correctly');
assert.equal(result.insertEscapedMarkdown, `[${defaultLabel}](${sampleUrl})`, 'Markdown not returned correctly');
});
}); });

View File

@@ -430,6 +430,7 @@ export abstract class GridTableBase<T> extends Disposable implements IView {
this.renderGridDataRowsRange(startIndex, count); this.renderGridDataRowsRange(startIndex, count);
}); });
this.dataProvider.dataRows = collection; this.dataProvider.dataRows = collection;
this.setFilterState();
this.table.updateRowCount(); this.table.updateRowCount();
await this.setupState(); await this.setupState();
} }
@@ -525,7 +526,7 @@ export abstract class GridTableBase<T> extends Disposable implements IView {
}; };
this.table.rerenderGrid(); this.table.rerenderGrid();
})); }));
if (this.configurationService.getValue<boolean>('workbench')['enablePreviewFeatures']) { if (this.enableFilteringFeature) {
this.filterPlugin = new HeaderFilter(); this.filterPlugin = new HeaderFilter();
attachButtonStyler(this.filterPlugin, this.themeService); attachButtonStyler(this.filterPlugin, this.themeService);
this.table.registerPlugin(this.filterPlugin); this.table.registerPlugin(this.filterPlugin);
@@ -686,15 +687,25 @@ export abstract class GridTableBase<T> extends Disposable implements IView {
public updateResult(resultSet: ResultSetSummary) { public updateResult(resultSet: ResultSetSummary) {
this._resultSet = resultSet; this._resultSet = resultSet;
if (this.table && this.visible) { if (this.table && this.visible) {
if (this.configurationService.getValue<boolean>('workbench')['enablePreviewFeatures'] && this.options.inMemoryDataProcessing && this.options.inMemoryDataCountThreshold < resultSet.rowCount) {
this.filterPlugin.enabled = false;
}
this.dataProvider.length = resultSet.rowCount; this.dataProvider.length = resultSet.rowCount;
this.setFilterState();
this.table.updateRowCount(); this.table.updateRowCount();
} }
this._onDidChange.fire(undefined); this._onDidChange.fire(undefined);
} }
private get enableFilteringFeature(): boolean {
return this.configurationService.getValue<boolean>('workbench')['enablePreviewFeatures'];
}
private setFilterState(): void {
if (this.enableFilteringFeature) {
const rowCount = this.table.getData().getLength();
this.filterPlugin.enabled = this.options.inMemoryDataProcessing
&& (this.options.inMemoryDataCountThreshold === undefined || this.options.inMemoryDataCountThreshold >= rowCount);
}
}
private generateContext(cell?: Slick.Cell): IGridActionContext { private generateContext(cell?: Slick.Cell): IGridActionContext {
const selection = this.selectionModel.getSelectedRanges(); const selection = this.selectionModel.getSelectedRanges();
return <IGridActionContext>{ return <IGridActionContext>{

View File

@@ -24,6 +24,10 @@ export function showRestore(accessor: ServicesAccessor, connection: IConnectionP
} }
export const RestoreFeatureName = 'restore'; export const RestoreFeatureName = 'restore';
export const restoreIsPreviewFeature = localize('restore.isPreviewFeature', "You must enable preview features in order to use restore");
export const restoreNotSupportedOutOfContext = localize('restore.commandNotSupportedOutsideContext', "Restore command is not supported outside of a server context. Please select a server or database and try again.");
export const restoreNotSupportedForAzure = localize('restore.commandNotSupported', "Restore command is not supported for Azure SQL databases.");
export class RestoreAction extends Task { export class RestoreAction extends Task {
public static readonly ID = RestoreFeatureName; public static readonly ID = RestoreFeatureName;
@@ -44,7 +48,7 @@ export class RestoreAction extends Task {
const configurationService = accessor.get<IConfigurationService>(IConfigurationService); const configurationService = accessor.get<IConfigurationService>(IConfigurationService);
const previewFeaturesEnabled: boolean = configurationService.getValue<{ enablePreviewFeatures: boolean }>('workbench').enablePreviewFeatures; const previewFeaturesEnabled: boolean = configurationService.getValue<{ enablePreviewFeatures: boolean }>('workbench').enablePreviewFeatures;
if (!previewFeaturesEnabled) { if (!previewFeaturesEnabled) {
return accessor.get<INotificationService>(INotificationService).info(localize('restore.isPreviewFeature', "You must enable preview features in order to use restore")); return accessor.get<INotificationService>(INotificationService).info(restoreIsPreviewFeature);
} }
let connectionManagementService = accessor.get<IConnectionManagementService>(IConnectionManagementService); let connectionManagementService = accessor.get<IConnectionManagementService>(IConnectionManagementService);
@@ -56,13 +60,16 @@ export class RestoreAction extends Task {
if (profile) { if (profile) {
const serverInfo = connectionManagementService.getServerInfo(profile.id); const serverInfo = connectionManagementService.getServerInfo(profile.id);
if (serverInfo && serverInfo.isCloud && profile.providerName === mssqlProviderName) { if (serverInfo && serverInfo.isCloud && profile.providerName === mssqlProviderName) {
return accessor.get<INotificationService>(INotificationService).info(localize('restore.commandNotSupported', "Restore command is not supported for Azure SQL databases.")); return accessor.get<INotificationService>(INotificationService).info(restoreNotSupportedForAzure);
} }
} }
const capabilitiesService = accessor.get(ICapabilitiesService); const capabilitiesService = accessor.get(ICapabilitiesService);
const instantiationService = accessor.get(IInstantiationService); const instantiationService = accessor.get(IInstantiationService);
profile = profile ? profile : new ConnectionProfile(capabilitiesService, profile); profile = profile ? profile : new ConnectionProfile(capabilitiesService, profile);
if (!profile.serverName) {
return accessor.get<INotificationService>(INotificationService).info(restoreNotSupportedOutOfContext);
}
return instantiationService.invokeFunction(showRestore, profile); return instantiationService.invokeFunction(showRestore, profile);
} }
} }

View File

@@ -126,14 +126,6 @@ export default () => `
<p> <p>
${escape(localize('welcomePage.documentationBody', "Visit the documentation center for quickstarts, how-to guides, and references for PowerShell, APIs, etc."))} ${escape(localize('welcomePage.documentationBody', "Visit the documentation center for quickstarts, how-to guides, and references for PowerShell, APIs, etc."))}
</p> </p>
<div class="link-header">
<a class="link ads-welcome-page-link" href="https://aka.ms/AzureSQLSurvey">
${escape(localize('welcomePage.productFeedback', "Product Feedback"))}<span class="icon-link themed-icon-alt"></span>
</a>
</div>
<p>
${escape(localize('welcomePage.productFeedbackBody', "Shape the future of the Azure SQL products you're using by completing the feedback survey!"))}
</p>
<div class="videos-container row"> <div class="videos-container row">
<h2>${escape(localize('welcomePage.videos', "Videos"))}</h2> <h2>${escape(localize('welcomePage.videos', "Videos"))}</h2>
<div class="flex flex-container-video"> <div class="flex flex-container-video">

View File

@@ -20,6 +20,6 @@ export class ModelFactory implements IModelFactory {
} }
public createClientSession(options: IClientSessionOptions): IClientSession { public createClientSession(options: IClientSessionOptions): IClientSession {
return new ClientSession(options); return this.instantiationService.createInstance(ClientSession, options);
} }
} }

View File

@@ -971,6 +971,12 @@ export class NotebookModel extends Disposable implements INotebookModel {
if (kernel.info) { if (kernel.info) {
this.updateLanguageInfo(kernel.info.language_info); this.updateLanguageInfo(kernel.info.language_info);
} }
this.adstelemetryService.createActionEvent(TelemetryKeys.TelemetryView.Notebook, TelemetryKeys.NbTelemetryAction.KernelChanged)
.withAdditionalProperties({
name: kernel.name,
alias: kernelAlias || ''
})
.send();
this._kernelChangedEmitter.fire({ this._kernelChangedEmitter.fire({
newValue: kernel, newValue: kernel,
oldValue: undefined, oldValue: undefined,

View File

@@ -58,7 +58,7 @@ export class MarkdownRenderer {
if (!markdown) { if (!markdown) {
element = document.createElement('span'); element = document.createElement('span');
} else { } else {
element = renderMarkdown(markdown, { ...this._getRenderOptions(disposeables), ...options }, markedOptions); element = renderMarkdown(markdown, { ...this._getRenderOptions(markdown, disposeables), ...options }, markedOptions);
} }
return { return {
@@ -67,7 +67,7 @@ export class MarkdownRenderer {
}; };
} }
protected _getRenderOptions(disposeables: DisposableStore): MarkdownRenderOptions { protected _getRenderOptions(markdown: IMarkdownString, disposeables: DisposableStore): MarkdownRenderOptions {
return { return {
baseUrl: this._options.baseUrl, baseUrl: this._options.baseUrl,
codeBlockRenderer: async (languageAlias, value) => { codeBlockRenderer: async (languageAlias, value) => {
@@ -105,7 +105,7 @@ export class MarkdownRenderer {
}, },
asyncRenderCallback: () => this._onDidRenderAsync.fire(), asyncRenderCallback: () => this._onDidRenderAsync.fire(),
actionHandler: { actionHandler: {
callback: (content) => this._openerService.open(content, { fromUserGesture: true }).catch(onUnexpectedError), callback: (content) => this._openerService.open(content, { fromUserGesture: true, allowContributedOpeners: true, allowCommands: markdown.isTrusted }).catch(onUnexpectedError),
disposeables disposeables
} }
}; };

View File

@@ -21,10 +21,15 @@ class CommandOpener implements IOpener {
constructor(@ICommandService private readonly _commandService: ICommandService) { } constructor(@ICommandService private readonly _commandService: ICommandService) { }
async open(target: URI | string) { async open(target: URI | string, options?: OpenOptions): Promise<boolean> {
if (!matchesScheme(target, Schemas.command)) { if (!matchesScheme(target, Schemas.command)) {
return false; return false;
} }
if (!options?.allowCommands) {
// silently ignore commands when command-links are disabled, also
// surpress other openers by returning TRUE
return true;
}
// run command or bail out if command isn't known // run command or bail out if command isn't known
if (typeof target === 'string') { if (typeof target === 'string') {
target = URI.parse(target); target = URI.parse(target);

View File

@@ -808,7 +808,23 @@ export class ViewModel extends Disposable implements IViewModel {
const fontInfo = this._configuration.options.get(EditorOption.fontInfo); const fontInfo = this._configuration.options.get(EditorOption.fontInfo);
const colorMap = this._getColorMap(); const colorMap = this._getColorMap();
const fontFamily = fontInfo.fontFamily === EDITOR_FONT_DEFAULTS.fontFamily ? fontInfo.fontFamily : `'${fontInfo.fontFamily}', ${EDITOR_FONT_DEFAULTS.fontFamily}`; const hasBadChars = (/[:;\\\/<>]/.test(fontInfo.fontFamily));
const useDefaultFontFamily = (hasBadChars || fontInfo.fontFamily === EDITOR_FONT_DEFAULTS.fontFamily);
let fontFamily: string;
if (useDefaultFontFamily) {
fontFamily = EDITOR_FONT_DEFAULTS.fontFamily;
} else {
fontFamily = fontInfo.fontFamily;
fontFamily = fontFamily.replace(/"/g, '\'');
const hasQuotesOrIsList = /[,']/.test(fontFamily);
if (!hasQuotesOrIsList) {
const needsQuotes = /[+ ]/.test(fontFamily);
if (needsQuotes) {
fontFamily = `'${fontFamily}'`;
}
}
fontFamily = `${fontFamily}, ${EDITOR_FONT_DEFAULTS.fontFamily}`;
}
return { return {
mode: languageId.language, mode: languageId.language,

View File

@@ -98,14 +98,19 @@ export class CodeLensContribution implements IEditorContribution {
const fontFamily = this._editor.getOption(EditorOption.codeLensFontFamily); const fontFamily = this._editor.getOption(EditorOption.codeLensFontFamily);
const editorFontInfo = this._editor.getOption(EditorOption.fontInfo); const editorFontInfo = this._editor.getOption(EditorOption.fontInfo);
const fontFamilyVar = `--codelens-font-family${this._styleClassName}`;
const fontFeaturesVar = `--codelens-font-features${this._styleClassName}`;
let newStyle = ` let newStyle = `
.monaco-editor .codelens-decoration.${this._styleClassName} { line-height: ${codeLensHeight}px; font-size: ${fontSize}px; padding-right: ${Math.round(fontSize * 0.5)}px; font-feature-settings: ${editorFontInfo.fontFeatureSettings} } .monaco-editor .codelens-decoration.${this._styleClassName} { line-height: ${codeLensHeight}px; font-size: ${fontSize}px; padding-right: ${Math.round(fontSize * 0.5)}px; font-feature-settings: var(${fontFeaturesVar}) }
.monaco-editor .codelens-decoration.${this._styleClassName} span.codicon { line-height: ${codeLensHeight}px; font-size: ${fontSize}px; } .monaco-editor .codelens-decoration.${this._styleClassName} span.codicon { line-height: ${codeLensHeight}px; font-size: ${fontSize}px; }
`; `;
if (fontFamily) { if (fontFamily) {
newStyle += `.monaco-editor .codelens-decoration.${this._styleClassName} { font-family: ${fontFamily}}`; newStyle += `.monaco-editor .codelens-decoration.${this._styleClassName} { font-family: ${fontFamily}}`;
} }
this._styleElement.textContent = newStyle; this._styleElement.textContent = newStyle;
this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily ?? 'inherit');
this._editor.getContainerDomNode().style.setProperty(fontFeaturesVar, editorFontInfo.fontFeatureSettings);
// //
this._editor.changeViewZones(accessor => { this._editor.changeViewZones(accessor => {

View File

@@ -143,7 +143,7 @@ class MessageWidget {
this._codeLink.setAttribute('href', `${code.target.toString()}`); this._codeLink.setAttribute('href', `${code.target.toString()}`);
this._codeLink.onclick = (e) => { this._codeLink.onclick = (e) => {
this._openerService.open(code.target); this._openerService.open(code.target, { allowCommands: true });
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
}; };

View File

@@ -536,7 +536,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget {
this._codeLink.setAttribute('href', code.target.toString()); this._codeLink.setAttribute('href', code.target.toString());
this._codeLink.onclick = (e) => { this._codeLink.onclick = (e) => {
this._openerService.open(code.target); this._openerService.open(code.target, { allowCommands: true });
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
}; };

View File

@@ -327,7 +327,7 @@ export class LinkDetector implements IEditorContribution {
} }
} }
return this.openerService.open(uri, { openToSide, fromUserGesture }); return this.openerService.open(uri, { openToSide, fromUserGesture, allowContributedOpeners: true, allowCommands: true });
}, err => { }, err => {
const messageOrError = const messageOrError =

View File

@@ -81,20 +81,10 @@ suite('OpenerService', function () {
const id = `aCommand${Math.random()}`; const id = `aCommand${Math.random()}`;
CommandsRegistry.registerCommand(id, function () { }); CommandsRegistry.registerCommand(id, function () { });
assert.strictEqual(lastCommand, undefined);
await openerService.open(URI.parse('command:' + id)); await openerService.open(URI.parse('command:' + id));
assert.equal(lastCommand!.id, id);
assert.equal(lastCommand!.args.length, 0);
await openerService.open(URI.parse('command:' + id).with({ query: '123' })); assert.strictEqual(lastCommand, undefined);
assert.equal(lastCommand!.id, id);
assert.equal(lastCommand!.args.length, 1);
assert.equal(lastCommand!.args[0], '123');
await openerService.open(URI.parse('command:' + id).with({ query: JSON.stringify([12, true]) }));
assert.equal(lastCommand!.id, id);
assert.equal(lastCommand!.args.length, 2);
assert.equal(lastCommand!.args[0], 12);
assert.equal(lastCommand!.args[1], true);
}); });
test('links are protected by validators', async function () { test('links are protected by validators', async function () {
@@ -108,6 +98,33 @@ suite('OpenerService', function () {
assert.equal(httpsResult, false); assert.equal(httpsResult, false);
}); });
test('delegate to commandsService, command:someid', async function () {
const openerService = new OpenerService(editorService, commandService);
const id = `aCommand${Math.random()}`;
CommandsRegistry.registerCommand(id, function () { });
await openerService.open(URI.parse('command:' + id).with({ query: '\"123\"' }), { allowCommands: true });
assert.strictEqual(lastCommand!.id, id);
assert.strictEqual(lastCommand!.args.length, 1);
assert.strictEqual(lastCommand!.args[0], '123');
await openerService.open(URI.parse('command:' + id), { allowCommands: true });
assert.strictEqual(lastCommand!.id, id);
assert.strictEqual(lastCommand!.args.length, 0);
await openerService.open(URI.parse('command:' + id).with({ query: '123' }), { allowCommands: true });
assert.strictEqual(lastCommand!.id, id);
assert.strictEqual(lastCommand!.args.length, 1);
assert.strictEqual(lastCommand!.args[0], 123);
await openerService.open(URI.parse('command:' + id).with({ query: JSON.stringify([12, true]) }), { allowCommands: true });
assert.strictEqual(lastCommand!.id, id);
assert.strictEqual(lastCommand!.args.length, 2);
assert.strictEqual(lastCommand!.args[0], 12);
assert.strictEqual(lastCommand!.args[1], true);
});
test('links validated by validators go to openers', async function () { test('links validated by validators go to openers', async function () {
const openerService = new OpenerService(editorService, commandService); const openerService = new OpenerService(editorService, commandService);

View File

@@ -50,7 +50,7 @@ export class Link extends Disposable {
this._register(onOpen(e => { this._register(onOpen(e => {
EventHelper.stop(e, true); EventHelper.stop(e, true);
openerService.open(link.href); openerService.open(link.href, { allowCommands: true });
})); }));
this.applyStyles(); this.applyStyles();

View File

@@ -29,9 +29,18 @@ type OpenInternalOptions = {
* action, such as keyboard or mouse usage. * action, such as keyboard or mouse usage.
*/ */
readonly fromUserGesture?: boolean; readonly fromUserGesture?: boolean;
/**
* Allow command links to be handled.
*/
readonly allowCommands?: boolean;
}; };
type OpenExternalOptions = { readonly openExternal?: boolean; readonly allowTunneling?: boolean }; export type OpenExternalOptions = {
readonly openExternal?: boolean;
readonly allowTunneling?: boolean;
readonly allowContributedOpeners?: boolean | string;
};
export type OpenOptions = OpenInternalOptions & OpenExternalOptions; export type OpenOptions = OpenInternalOptions & OpenExternalOptions;

View File

@@ -18,6 +18,7 @@ export interface IRemoteAgentEnvironment {
workspaceStorageHome: URI; workspaceStorageHome: URI;
userHome: URI; userHome: URI;
os: OperatingSystem; os: OperatingSystem;
useHostProxy: boolean;
} }
export interface RemoteAgentConnectionContext { export interface RemoteAgentConnectionContext {

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