mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 11:01:37 -05:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13e3627627 | ||
|
|
e2111fe493 | ||
|
|
7b46269b44 | ||
|
|
69b9a19634 | ||
|
|
ebe835ec99 | ||
|
|
d4e25f4d89 | ||
|
|
809d4de862 | ||
|
|
e1aae951a3 | ||
|
|
bdd99bd0d8 | ||
|
|
c06bfad916 | ||
|
|
1c91f7971e | ||
|
|
8c9037fbdf | ||
|
|
d029bb9602 | ||
|
|
f0558714a4 | ||
|
|
49147305e8 | ||
|
|
519012c690 | ||
|
|
ff05bc2b03 | ||
|
|
b54beb6e7a | ||
|
|
dbdcc3d20a | ||
|
|
cf48776710 | ||
|
|
309f750b92 | ||
|
|
8c92af3016 | ||
|
|
9b117da9cb | ||
|
|
d12a7b81fd | ||
|
|
3eb705e77b | ||
|
|
b421b19b73 | ||
|
|
124f7ca887 |
@@ -151,7 +151,7 @@ steps:
|
||||
inputs:
|
||||
ConnectedServiceName: 'Code Signing'
|
||||
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
|
||||
inlineOperation: |
|
||||
[
|
||||
|
||||
@@ -114,6 +114,8 @@ const indentationFilter = [
|
||||
'!extensions/big-data-cluster/src/bigDataCluster/controller/apiGenerated.ts',
|
||||
'!extensions/big-data-cluster/src/bigDataCluster/controller/clusterApiGenerated2.ts',
|
||||
'!resources/linux/snap/electron-launch',
|
||||
'!extensions/markdown-language-features/media/*.js',
|
||||
'!extensions/simple-browser/media/*.js',
|
||||
'!resources/xlf/LocProject.json', // {{SQL CARBON EDIT}}
|
||||
'!build/**/*' // {{SQL CARBON EDIT}}
|
||||
];
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
title: Azure Arc Data Services
|
||||
description: A collection of notebooks to support Azure Arc Data Services.
|
||||
title: Azure Arc Data Services
|
||||
@@ -1,12 +1,10 @@
|
||||
- title: Welcome
|
||||
url: /readme
|
||||
not_numbered: true
|
||||
- title: Search
|
||||
search: true
|
||||
- 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
|
||||
- title: Postgres
|
||||
url: /postgres/readme
|
||||
not_numbered: true
|
||||
sections:
|
||||
- title: TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter
|
||||
url: postgres/tsg100-troubleshoot-postgres
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
- 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)
|
||||
|
||||
@@ -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
|
||||
@@ -2,7 +2,11 @@
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"TSG100 - The Azure Arc enabled PostgreSQL Hyperscale troubleshooter\n",
|
||||
"===================================================================\n",
|
||||
@@ -35,14 +39,17 @@
|
||||
"# the user will be prompted to select a server.\n",
|
||||
"namespace = os.environ.get('POSTGRES_SERVER_NAMESPACE')\n",
|
||||
"name = os.environ.get('POSTGRES_SERVER_NAME')\n",
|
||||
"version = os.environ.get('POSTGRES_SERVER_VERSION')\n",
|
||||
"\n",
|
||||
"tail_lines = 50"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"### Common functions\n",
|
||||
"\n",
|
||||
@@ -63,7 +70,6 @@
|
||||
"import sys\n",
|
||||
"import os\n",
|
||||
"import re\n",
|
||||
"import json\n",
|
||||
"import platform\n",
|
||||
"import shlex\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",
|
||||
"install_hint = {} # The SOP to help install the executable if it cannot be found\n",
|
||||
"\n",
|
||||
"first_run = True\n",
|
||||
"rules = None\n",
|
||||
"debug_logging = False\n",
|
||||
"\n",
|
||||
"def run(cmd, return_output=False, no_output=False, retry_count=0):\n",
|
||||
"def run(cmd, return_output=False, no_output=False, retry_count=0, base64_decode=False, return_as_json=False):\n",
|
||||
" \"\"\"Run shell command, stream stdout, print stderr and optionally return output\n",
|
||||
"\n",
|
||||
" NOTES:\n",
|
||||
@@ -103,13 +105,6 @@
|
||||
" output = \"\"\n",
|
||||
" retry = False\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",
|
||||
" #\n",
|
||||
" # ('HY090', '[HY090] [Microsoft][ODBC Driver Manager] Invalid string or buffer length (0) (SQLExecDirectW)')\n",
|
||||
@@ -172,7 +167,12 @@
|
||||
" if which_binary == None:\n",
|
||||
" which_binary = shutil.which(cmd_actual[0])\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",
|
||||
" 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",
|
||||
" 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",
|
||||
@@ -219,8 +219,6 @@
|
||||
" break # otherwise infinite hang, have not worked out why yet.\n",
|
||||
" else:\n",
|
||||
" print(line, end='')\n",
|
||||
" if rules is not None:\n",
|
||||
" apply_expert_rules(line)\n",
|
||||
"\n",
|
||||
" if wait:\n",
|
||||
" p.wait()\n",
|
||||
@@ -276,25 +274,22 @@
|
||||
" 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",
|
||||
"\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",
|
||||
" #\n",
|
||||
" if user_provided_exe_name in retry_hints:\n",
|
||||
" for retry_hint in retry_hints[user_provided_exe_name]:\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",
|
||||
" retry_count = retry_count + 1\n",
|
||||
" output = run(cmd, return_output=return_output, retry_count=retry_count)\n",
|
||||
"\n",
|
||||
" if return_output:\n",
|
||||
" return output\n",
|
||||
" else:\n",
|
||||
" return\n",
|
||||
" if base64_decode:\n",
|
||||
" import base64\n",
|
||||
" return base64.b64decode(output).decode('utf-8')\n",
|
||||
" else:\n",
|
||||
" return output\n",
|
||||
"\n",
|
||||
" elapsed = datetime.datetime.now().replace(microsecond=0) - start_time\n",
|
||||
"\n",
|
||||
@@ -311,78 +306,31 @@
|
||||
" print(f'\\nSUCCESS: {elapsed}s elapsed.\\n')\n",
|
||||
"\n",
|
||||
" if return_output:\n",
|
||||
" return output\n",
|
||||
"\n",
|
||||
"def load_json(filename):\n",
|
||||
" \"\"\"Load a json file from disk and return the contents\"\"\"\n",
|
||||
"\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",
|
||||
" if base64_decode:\n",
|
||||
" import base64\n",
|
||||
" return base64.b64decode(output).decode('utf-8')\n",
|
||||
" else:\n",
|
||||
" return output\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print('Common functions defined successfully.')\n",
|
||||
"\n",
|
||||
"# Hints for binary (transient fault) retry, (known) error and install guide\n",
|
||||
"# Hints for tool retry (on transient fault), known errors and install guide\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",
|
||||
"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",
|
||||
"install_hint = {'kubectl': ['SOP036 - Install kubectl command line interface', '../install/sop036-install-kubectl.ipynb']}"
|
||||
"retry_hints = {}\n",
|
||||
"error_hints = {}\n",
|
||||
"install_hint = {}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print('Common functions defined successfully.')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"### Get Postgres server"
|
||||
]
|
||||
@@ -400,10 +348,11 @@
|
||||
"# Sets the 'server' variable to the spec of the Postgres server\n",
|
||||
"\n",
|
||||
"import math\n",
|
||||
"import json\n",
|
||||
"\n",
|
||||
"# If a server was provided, get it\n",
|
||||
"if namespace and name and version:\n",
|
||||
" server = json.loads(run(f'kubectl get postgresql-{version} -n {namespace} {name} -o json', return_output=True))\n",
|
||||
"if namespace and name:\n",
|
||||
" server = json.loads(run(f'kubectl get postgresqls -n {namespace} {name} -o json', return_output=True))\n",
|
||||
"else:\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",
|
||||
@@ -415,19 +364,18 @@
|
||||
"\n",
|
||||
" pad = math.floor(math.log10(len(servers)) + 1) + 3\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",
|
||||
" while True:\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",
|
||||
" continue\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",
|
||||
" namespace = server['metadata']['namespace']\n",
|
||||
" name = server['metadata']['name']\n",
|
||||
" version = server['kind'][len('postgresql-'):]\n",
|
||||
" break\n",
|
||||
"\n",
|
||||
"display(Markdown(f'#### Got server {namespace}.{name}'))"
|
||||
@@ -435,7 +383,11 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"### Summarize all resources"
|
||||
]
|
||||
@@ -443,13 +395,15 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"uid = server['metadata']['uid']\n",
|
||||
"\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",
|
||||
"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}')"
|
||||
@@ -457,7 +411,11 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"### Troubleshoot the server"
|
||||
]
|
||||
@@ -465,16 +423,22 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"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",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"### Troubleshoot the pods"
|
||||
]
|
||||
@@ -482,7 +446,9 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"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",
|
||||
@@ -505,7 +471,11 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"### Troubleshoot the containers"
|
||||
]
|
||||
@@ -513,7 +483,9 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Summarize and get logs from each container\n",
|
||||
@@ -521,7 +493,7 @@
|
||||
" pod_name = pod['metadata']['name']\n",
|
||||
" cons = pod['spec']['containers']\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",
|
||||
"\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",
|
||||
"\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",
|
||||
" run(f'kubectl logs -n {namespace} {pod_name} {con_name} --tail {tail_lines} --previous')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"### Troubleshoot the PersistentVolumeClaims"
|
||||
]
|
||||
@@ -552,7 +528,9 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"display(Markdown(f'#### Troubleshooting PersistentVolumeClaims'))\n",
|
||||
@@ -562,10 +540,12 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print('Notebook execution complete.')"
|
||||
"print(\"Notebook execution is complete.\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -576,20 +556,36 @@
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
},
|
||||
"azdata": {
|
||||
"pansop": {
|
||||
"related": "",
|
||||
"test": {
|
||||
"ci": false,
|
||||
"gci": false
|
||||
},
|
||||
"contract": {
|
||||
"requires": {
|
||||
"kubectl": {
|
||||
"installed": true
|
||||
}
|
||||
"strategy": "",
|
||||
"types": null,
|
||||
"disable": {
|
||||
"reason": "",
|
||||
"workitems": null,
|
||||
"types": null
|
||||
}
|
||||
},
|
||||
"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": []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# Azure Arc Data Services Jupyter Book
|
||||
|
||||
## Chapters
|
||||
|
||||
1. [Postgres](postgres/readme.md) - notebooks for troubleshooting Postgres on Azure Arc.
|
||||
@@ -2,14 +2,14 @@
|
||||
"name": "arc",
|
||||
"displayName": "%arc.displayName%",
|
||||
"description": "%arc.description%",
|
||||
"version": "0.9.0",
|
||||
"version": "0.9.3",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
"icon": "images/extension.png",
|
||||
"engines": {
|
||||
"vscode": "*",
|
||||
"azdata": ">=1.27.0"
|
||||
"azdata": ">=1.28.0"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onCommand:arc.connectToController",
|
||||
@@ -520,7 +520,7 @@
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.3.1"
|
||||
"version": "20.3.2"
|
||||
}
|
||||
],
|
||||
"when": true
|
||||
@@ -772,7 +772,7 @@
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.3.1"
|
||||
"version": "20.3.2"
|
||||
}
|
||||
],
|
||||
"when": "true"
|
||||
@@ -1001,7 +1001,7 @@
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.3.1"
|
||||
"version": "20.3.2"
|
||||
}
|
||||
],
|
||||
"when": "mi-type=arc-mi"
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as loc from '../localizedConstants';
|
||||
import { throwUnless } from './utils';
|
||||
export interface KubeClusterContext {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
isCurrentContext: boolean;
|
||||
}
|
||||
|
||||
@@ -18,7 +19,7 @@ export interface KubeClusterContext {
|
||||
*
|
||||
* @param configFile
|
||||
*/
|
||||
export function getKubeConfigClusterContexts(configFile: string): Promise<KubeClusterContext[]> {
|
||||
export function getKubeConfigClusterContexts(configFile: string): KubeClusterContext[] {
|
||||
const config: any = yamljs.load(configFile);
|
||||
const rawContexts = <any[]>config['contexts'];
|
||||
throwUnless(rawContexts && rawContexts.length, loc.noContextFound(configFile));
|
||||
@@ -26,16 +27,16 @@ export function getKubeConfigClusterContexts(configFile: string): Promise<KubeCl
|
||||
throwUnless(currentContext, loc.noCurrentContextFound(configFile));
|
||||
const contexts: KubeClusterContext[] = [];
|
||||
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));
|
||||
if (name) {
|
||||
contexts.push({
|
||||
name: name,
|
||||
isCurrentContext: name === currentContext
|
||||
});
|
||||
}
|
||||
contexts.push({
|
||||
name: name,
|
||||
namespace: namespace,
|
||||
isCurrentContext: name === currentContext
|
||||
});
|
||||
});
|
||||
return Promise.resolve(contexts);
|
||||
return contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,22 +48,23 @@ export function getKubeConfigClusterContexts(configFile: string): Promise<KubeCl
|
||||
*
|
||||
*
|
||||
* @param clusterContexts
|
||||
* @param previousClusterContext
|
||||
* @param previousClusterContextName
|
||||
* @param throwIfNotFound
|
||||
*/
|
||||
export function getCurrentClusterContext(clusterContexts: KubeClusterContext[], previousClusterContext?: string, throwIfNotFound: boolean = false): string {
|
||||
if (previousClusterContext) {
|
||||
if (clusterContexts.find(c => c.name === previousClusterContext)) { // if previous cluster context value is found in clusters then return that value
|
||||
export function getCurrentClusterContext(clusterContexts: KubeClusterContext[], previousClusterContextName?: string, throwIfNotFound: boolean = false): KubeClusterContext {
|
||||
if (previousClusterContextName) {
|
||||
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;
|
||||
} else {
|
||||
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
|
||||
const currentClusterContext = clusterContexts.find(c => c.isCurrentContext)?.name;
|
||||
const currentClusterContext = clusterContexts.find(c => c.isCurrentContext);
|
||||
throwUnless(currentClusterContext !== undefined, loc.noCurrentClusterContext);
|
||||
return currentClusterContext;
|
||||
}
|
||||
|
||||
@@ -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 const passwordToController = localize('arc.passwordToController', "Provide Password to Controller");
|
||||
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 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 controllerClusterContext = localize('arc.controllerClusterContext', "Cluster Context");
|
||||
export const defaultControllerName = localize('arc.defaultControllerName', "arc-dc");
|
||||
export const postgresProviderName = localize('arc.postgresProviderName', "PGSQL");
|
||||
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 password = localize('arc.password', "Password");
|
||||
export const rememberPassword = localize('arc.rememberPassword', "Remember Password");
|
||||
|
||||
@@ -46,6 +46,20 @@ export class ControllerModel {
|
||||
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) {
|
||||
this._info = value;
|
||||
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.
|
||||
* @param promptReconnect
|
||||
*/
|
||||
public async acquireAzdataSession(promptReconnect: boolean = false): Promise<azdataExt.AzdataSession> {
|
||||
public async login(promptReconnect: boolean = false): Promise<void> {
|
||||
let promptForValidClusterContext: boolean = false;
|
||||
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'
|
||||
} catch (error) {
|
||||
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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this._azdataApi.azdata.acquireSession(this.info.url, this.info.username, this._password, this.azdataAdditionalEnvVars);
|
||||
await this._azdataApi.azdata.login({ endpoint: this.info.endpoint, namespace: this.info.namespace }, this.info.username, this._password, this.azdataAdditionalEnvVars);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,67 +128,64 @@ export class ControllerModel {
|
||||
await this.refresh(false);
|
||||
}
|
||||
}
|
||||
public async refresh(showErrors: boolean = true, promptReconnect: boolean = false): Promise<void> {
|
||||
const session = await this.acquireAzdataSession(promptReconnect);
|
||||
public async refresh(showErrors: boolean = true): Promise<void> {
|
||||
// First need to log in to ensure that we're able to authenticate with the controller
|
||||
await this.login(false);
|
||||
const newRegistrations: Registration[] = [];
|
||||
try {
|
||||
await Promise.all([
|
||||
this._azdataApi.azdata.arc.dc.config.show(this.azdataAdditionalEnvVars, session).then(result => {
|
||||
this._controllerConfig = result.result;
|
||||
this.configLastUpdated = new Date();
|
||||
this._onConfigUpdated.fire(this._controllerConfig);
|
||||
}).catch(err => {
|
||||
// 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
|
||||
// loading icon forever)
|
||||
if (showErrors) {
|
||||
vscode.window.showErrorMessage(loc.fetchConfigFailed(this.info.name, err));
|
||||
}
|
||||
this._onConfigUpdated.fire(this._controllerConfig);
|
||||
throw err;
|
||||
await Promise.all([
|
||||
this._azdataApi.azdata.arc.dc.config.show(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
|
||||
this._controllerConfig = result.result;
|
||||
this.configLastUpdated = new Date();
|
||||
this._onConfigUpdated.fire(this._controllerConfig);
|
||||
}).catch(err => {
|
||||
// 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
|
||||
// loading icon forever)
|
||||
if (showErrors) {
|
||||
vscode.window.showErrorMessage(loc.fetchConfigFailed(this.info.name, err));
|
||||
}
|
||||
this._onConfigUpdated.fire(this._controllerConfig);
|
||||
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._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, 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);
|
||||
this._azdataApi.azdata.arc.sql.mi.list(this.azdataAdditionalEnvVars, this.controllerContext).then(result => {
|
||||
newRegistrations.push(...result.result.map(r => {
|
||||
return {
|
||||
instanceName: r.name,
|
||||
state: r.state,
|
||||
instanceType: ResourceType.sqlManagedInstances
|
||||
};
|
||||
}));
|
||||
})
|
||||
]);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
]).then(() => {
|
||||
this._registrations = newRegistrations;
|
||||
this.registrationsLastUpdated = new Date();
|
||||
this._onRegistrationsUpdated.fire(this._registrations);
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
public get endpoints(): azdataExt.DcEndpointListResult[] {
|
||||
@@ -204,6 +214,6 @@ export class ControllerModel {
|
||||
* property to for use a display label for this controller
|
||||
*/
|
||||
public get label(): string {
|
||||
return `${this.info.name} (${this.info.url})`;
|
||||
return `${this.info.name} (${this.controllerContext})`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,11 +71,9 @@ export class MiaaModel extends ResourceModel {
|
||||
return this._refreshPromise.promise;
|
||||
}
|
||||
this._refreshPromise = new Deferred();
|
||||
let session: azdataExt.AzdataSession | undefined = undefined;
|
||||
try {
|
||||
session = await this.controllerModel.acquireAzdataSession();
|
||||
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.configLastUpdated = new Date();
|
||||
this._onConfigUpdated.fire(this._config);
|
||||
@@ -109,7 +107,6 @@ export class MiaaModel extends ResourceModel {
|
||||
this._refreshPromise.reject(err);
|
||||
throw err;
|
||||
} finally {
|
||||
session?.dispose();
|
||||
this._refreshPromise = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,10 +53,7 @@ export class PostgresModel extends ResourceModel {
|
||||
|
||||
/** Returns the major version of Postgres */
|
||||
public get engineVersion(): string | undefined {
|
||||
const kind = this._config?.kind;
|
||||
return kind
|
||||
? kind.substring(kind.lastIndexOf('-') + 1)
|
||||
: undefined;
|
||||
return this._config?.spec.engine.version;
|
||||
}
|
||||
|
||||
/** Returns the IP address and port of Postgres */
|
||||
@@ -121,10 +118,8 @@ export class PostgresModel extends ResourceModel {
|
||||
return this._refreshPromise.promise;
|
||||
}
|
||||
this._refreshPromise = new Deferred();
|
||||
let session: azdataExt.AzdataSession | undefined = undefined;
|
||||
try {
|
||||
session = await this.controllerModel.acquireAzdataSession();
|
||||
this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, session)).result;
|
||||
this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name, this.controllerModel.azdataAdditionalEnvVars, this.controllerModel.controllerContext)).result;
|
||||
this.configLastUpdated = new Date();
|
||||
this._onConfigUpdated.fire(this._config);
|
||||
this._refreshPromise.resolve();
|
||||
@@ -132,7 +127,6 @@ export class PostgresModel extends ResourceModel {
|
||||
this._refreshPromise.reject(err);
|
||||
throw err;
|
||||
} finally {
|
||||
session?.dispose();
|
||||
this._refreshPromise = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourcePro
|
||||
const controller = (await getRegisteredDataControllers(this._treeProvider)).find(ci => ci.label === controllerLabel);
|
||||
throwUnless(controller !== undefined, loc.noControllerInfoFound(controllerLabel));
|
||||
switch (variableName) {
|
||||
case 'endpoint': return controller.info.url;
|
||||
case 'endpoint': return controller.info.endpoint || '';
|
||||
case 'username': return controller.info.username;
|
||||
case 'kubeConfig': return controller.info.kubeConfigFilePath;
|
||||
case 'clusterContext': return controller.info.kubeClusterContext;
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('KubeUtils', function (): void {
|
||||
contexts[1].name.should.equal('kubernetes-admin@kubernetes', `test: ${testName} failed`);
|
||||
contexts[1].isCurrentContext.should.be.false(`test: ${testName} failed`);
|
||||
};
|
||||
verifyContexts(await getKubeConfigClusterContexts(configFile), 'getKubeConfigClusterContexts');
|
||||
verifyContexts(getKubeConfigClusterContexts(configFile), 'getKubeConfigClusterContexts');
|
||||
});
|
||||
it('throws error when unable to load config file', async () => {
|
||||
const error = new Error('unknown error accessing file');
|
||||
|
||||
@@ -23,9 +23,9 @@ export class FakeAzdataApi implements azdataExt.IAzdataApi {
|
||||
},
|
||||
postgres: {
|
||||
server: {
|
||||
postgresInstances: [],
|
||||
postgresInstances: <azdataExt.PostgresServerListResult[]>[],
|
||||
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.'); },
|
||||
edit(
|
||||
_name: string,
|
||||
@@ -42,16 +42,15 @@ export class FakeAzdataApi implements azdataExt.IAzdataApi {
|
||||
replaceEngineSettings?: boolean,
|
||||
workers?: number
|
||||
},
|
||||
_engineVersion?: string,
|
||||
_additionalEnvVars?: azdataExt.AdditionalEnvVars
|
||||
): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
|
||||
}
|
||||
},
|
||||
sql: {
|
||||
mi: {
|
||||
miaaInstances: [],
|
||||
miaaInstances: <azdataExt.SqlMiListResult[]>[],
|
||||
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.'); },
|
||||
edit(
|
||||
_name: string,
|
||||
@@ -66,17 +65,14 @@ export class FakeAzdataApi implements azdataExt.IAzdataApi {
|
||||
}
|
||||
};
|
||||
|
||||
// public postgresInstances: 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[]) {
|
||||
this._arcApi.sql.mi.miaaInstances = <any>instances;
|
||||
this._arcApi.sql.mi.miaaInstances = instances;
|
||||
}
|
||||
|
||||
// public miaaInstances: azdataExt.SqlMiListResult[] = [];
|
||||
|
||||
//
|
||||
// API Implementation
|
||||
//
|
||||
@@ -86,12 +82,9 @@ export class FakeAzdataApi implements azdataExt.IAzdataApi {
|
||||
getPath(): Promise<string> {
|
||||
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;
|
||||
}
|
||||
acquireSession(_endpoint: string, _username: string, _password: string): Promise<azdataExt.AzdataSession> {
|
||||
return Promise.resolve({ dispose: () => { } });
|
||||
}
|
||||
version(): Promise<azdataExt.AzdataOutput<string>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { AzureArcTreeDataProvider } from '../../ui/tree/azureArcTreeDataProvider
|
||||
export class FakeControllerModel extends ControllerModel {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,20 @@ interface ExtensionGlobalMemento extends vscode.Memento {
|
||||
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 {
|
||||
afterEach(function (): void {
|
||||
sinon.restore();
|
||||
@@ -39,15 +53,15 @@ describe('ControllerModel', function (): void {
|
||||
|
||||
beforeEach(function (): void {
|
||||
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);
|
||||
});
|
||||
|
||||
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"
|
||||
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: [] });
|
||||
await should(model.acquireAzdataSession()).be.rejectedWith(new UserCancelledError(loc.userCancelledError));
|
||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), getDefaultControllerInfo());
|
||||
await should(model.login()).be.rejectedWith(new UserCancelledError(loc.userCancelledError));
|
||||
});
|
||||
|
||||
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 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);
|
||||
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();
|
||||
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
await model.login();
|
||||
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> {
|
||||
@@ -83,18 +97,18 @@ describe('ControllerModel', function (): void {
|
||||
|
||||
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
|
||||
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);
|
||||
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
|
||||
|
||||
// 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 }));
|
||||
|
||||
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();
|
||||
azdataMock.verify(x => x.acquireSession(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
|
||||
await model.login();
|
||||
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> {
|
||||
@@ -108,19 +122,19 @@ describe('ControllerModel', function (): void {
|
||||
|
||||
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
|
||||
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);
|
||||
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
|
||||
|
||||
// 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 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');
|
||||
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> {
|
||||
@@ -134,20 +148,20 @@ describe('ControllerModel', function (): void {
|
||||
|
||||
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
|
||||
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);
|
||||
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
|
||||
|
||||
// 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 }));
|
||||
|
||||
// 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');
|
||||
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> {
|
||||
@@ -162,7 +176,7 @@ describe('ControllerModel', function (): void {
|
||||
|
||||
const azdataExtApiMock = TypeMoq.Mock.ofType<azdataExt.IExtension>();
|
||||
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);
|
||||
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
|
||||
|
||||
@@ -170,27 +184,19 @@ describe('ControllerModel', function (): void {
|
||||
const originalPassword = 'originalPassword';
|
||||
const model = new ControllerModel(
|
||||
treeDataProvider,
|
||||
{
|
||||
id: uuid(),
|
||||
url: '127.0.0.1',
|
||||
kubeConfigFilePath: '/path/to/.kube/config',
|
||||
kubeClusterContext: 'currentCluster',
|
||||
username: 'admin',
|
||||
name: 'arc',
|
||||
rememberPassword: false,
|
||||
resources: []
|
||||
},
|
||||
getDefaultControllerInfo(),
|
||||
originalPassword
|
||||
);
|
||||
await treeDataProvider.addOrUpdateController(model, originalPassword);
|
||||
|
||||
const newInfo: ControllerInfo = {
|
||||
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',
|
||||
kubeClusterContext: 'currentCluster',
|
||||
username: 'newUser',
|
||||
name: 'newName',
|
||||
namespace: 'newNamespace',
|
||||
rememberPassword: true,
|
||||
resources: []
|
||||
};
|
||||
@@ -203,7 +209,7 @@ describe('ControllerModel', function (): void {
|
||||
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(
|
||||
{ controllerModel: newModel, password: newPassword }));
|
||||
|
||||
await model.acquireAzdataSession(true);
|
||||
await model.login(true);
|
||||
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(model.info).deepEqual(newInfo, 'Model info should have been updated');
|
||||
|
||||
@@ -40,7 +40,8 @@ export const FakePostgresServerShowOutput: azdataExt.AzdataOutput<azdataExt.Post
|
||||
extensions: [{ name: '' }],
|
||||
settings: {
|
||||
default: { ['']: '' }
|
||||
}
|
||||
},
|
||||
version: ''
|
||||
},
|
||||
scale: {
|
||||
shards: 0,
|
||||
@@ -114,7 +115,7 @@ describe('PostgresModel', function (): void {
|
||||
controllerModel = new FakeControllerModel();
|
||||
|
||||
//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
|
||||
azdataApi = new FakeAzdataApi();
|
||||
|
||||
@@ -38,7 +38,8 @@ export const FakePostgresServerShowOutput: azdataExt.AzdataOutput<azdataExt.Post
|
||||
extensions: [{ name: '' }],
|
||||
settings: {
|
||||
default: { ['']: '' }
|
||||
}
|
||||
},
|
||||
version: '12'
|
||||
},
|
||||
scale: {
|
||||
shards: 0,
|
||||
@@ -121,7 +122,7 @@ describe('postgresConnectionStringsPage', function (): void {
|
||||
controllerModel = new FakeControllerModel();
|
||||
|
||||
//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
|
||||
const postgresResource: PGResourceInfo = { name: 'pgt', resourceType: '' };
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('postgresOverviewPage', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
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');
|
||||
});
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ describe('ConnectControllerDialog', function (): void {
|
||||
|
||||
(<{ info: ControllerInfo | undefined, description: string }[]>[
|
||||
{ info: undefined, description: 'all input' },
|
||||
{ info: { url: '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' }, description: 'all but URL' },
|
||||
{ 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> {
|
||||
const connectControllerDialog = new ConnectToControllerDialog(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> {
|
||||
sinon.stub(ControllerModel.prototype, 'refresh').returns(Promise.reject('Controller refresh failed'));
|
||||
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');
|
||||
await connectControllerDialog.isInitialized;
|
||||
const validateResult = await connectControllerDialog.validate();
|
||||
@@ -41,36 +41,36 @@ describe('ConnectControllerDialog', function (): void {
|
||||
|
||||
it('validate replaces http with https', async function (): Promise<void> {
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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> {
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
||||
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(
|
||||
{ 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',
|
||||
undefined);
|
||||
});
|
||||
@@ -92,6 +92,6 @@ async function validateConnectControllerDialog(info: ControllerInfo, expectedUrl
|
||||
const validateResult = await connectControllerDialog.validate();
|
||||
should(validateResult).be.true('Validation should have returned true');
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,20 @@ interface ExtensionGlobalMemento extends vscode.Memento {
|
||||
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 {
|
||||
let treeDataProvider: AzureArcTreeDataProvider;
|
||||
beforeEach(function (): void {
|
||||
@@ -58,7 +72,7 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
||||
treeDataProvider['_loading'] = false;
|
||||
let children = await treeDataProvider.getChildren();
|
||||
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, '');
|
||||
should(children.length).equal(1, 'Controller node should be added correctly');
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
@@ -69,12 +83,12 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
||||
treeDataProvider['_loading'] = false;
|
||||
let children = await treeDataProvider.getChildren();
|
||||
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);
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
should(children.length).equal(1, 'Controller node should be added correctly');
|
||||
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);
|
||||
await treeDataProvider.addOrUpdateController(controllerModel2, '');
|
||||
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);
|
||||
|
||||
sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object);
|
||||
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').resolves([{ 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');
|
||||
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').returns([{ name: 'currentCluster', isCurrentContext: true }]);
|
||||
const controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo(), 'mypassword');
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
const controllerNode = treeDataProvider.getControllerNode(controllerModel);
|
||||
const children = await treeDataProvider.getChildren(controllerNode);
|
||||
@@ -123,8 +137,10 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
||||
describe('removeController', function (): void {
|
||||
it('removing a controller should work as expected', async function (): Promise<void> {
|
||||
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 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 controllerModel = new ControllerModel(treeDataProvider, getDefaultControllerInfo());
|
||||
const info2 = getDefaultControllerInfo();
|
||||
info2.username = 'cloudsa';
|
||||
const controllerModel2 = new ControllerModel(treeDataProvider, info2);
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
await treeDataProvider.addOrUpdateController(controllerModel2, '');
|
||||
const children = <ControllerTreeNode[]>(await treeDataProvider.getChildren());
|
||||
@@ -141,20 +157,20 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
||||
|
||||
describe('openResourceDashboard', function (): 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, '');
|
||||
await should(openDashboardPromise).be.rejected();
|
||||
});
|
||||
|
||||
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, '');
|
||||
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
|
||||
await should(openDashboardPromise).be.rejected();
|
||||
});
|
||||
|
||||
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);
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
const controllerNode = treeDataProvider.getControllerNode(controllerModel)!;
|
||||
|
||||
3
extensions/arc/src/typings/arc.d.ts
vendored
3
extensions/arc/src/typings/arc.d.ts
vendored
@@ -37,7 +37,8 @@ declare module 'arc' {
|
||||
id: string,
|
||||
kubeConfigFilePath: string,
|
||||
kubeClusterContext: string
|
||||
url: string,
|
||||
endpoint: string | undefined,
|
||||
namespace: string,
|
||||
name: string,
|
||||
username: string,
|
||||
rememberPassword: boolean,
|
||||
|
||||
@@ -17,6 +17,9 @@ export class RadioOptionsGroup {
|
||||
private _loadingBuilder: azdata.LoadingComponentBuilder;
|
||||
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++}`) {
|
||||
this._divContainer = this._modelBuilder.divContainer().withProperties<azdata.DivContainerProperties>({ clickable: false }).component();
|
||||
this._loadingBuilder = this._modelBuilder.loadingComponent().withItem(this._divContainer);
|
||||
@@ -26,7 +29,7 @@ export class RadioOptionsGroup {
|
||||
return this._loadingBuilder.component();
|
||||
}
|
||||
|
||||
async load(optionsInfoGetter: () => Promise<RadioOptionsInfo>): Promise<void> {
|
||||
async load(optionsInfoGetter: () => RadioOptionsInfo | Promise<RadioOptionsInfo>): Promise<void> {
|
||||
this.component().loading = true;
|
||||
this._divContainer.clearItems();
|
||||
try {
|
||||
@@ -51,6 +54,7 @@ export class RadioOptionsGroup {
|
||||
// it is just better to keep things clean.
|
||||
this._currentRadioOption.checked = false;
|
||||
this._currentRadioOption = radioOption;
|
||||
this._onRadioOptionChanged.fire(this.value);
|
||||
}
|
||||
}));
|
||||
this._divContainer.addItem(radioOption);
|
||||
|
||||
@@ -129,16 +129,12 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
let session: azdataExt.AzdataSession | undefined = undefined;
|
||||
try {
|
||||
session = await this._miaaModel.controllerModel.acquireAzdataSession();
|
||||
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) {
|
||||
this.saveButton!.enabled = true;
|
||||
throw err;
|
||||
} finally {
|
||||
session?.dispose();
|
||||
}
|
||||
try {
|
||||
await this._miaaModel.refresh();
|
||||
|
||||
@@ -244,12 +244,7 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token) => {
|
||||
const session = await this._controllerModel.acquireAzdataSession();
|
||||
try {
|
||||
return await this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name, this._controllerModel.azdataAdditionalEnvVars, session);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
return await this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name, this._controllerModel.azdataAdditionalEnvVars, this._controllerModel.controllerContext);
|
||||
}
|
||||
);
|
||||
await this._controllerModel.refreshTreeNode();
|
||||
|
||||
@@ -179,9 +179,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
let session: azdataExt.AzdataSession | undefined = undefined;
|
||||
try {
|
||||
session = await this._postgresModel.controllerModel.acquireAzdataSession();
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{
|
||||
@@ -191,10 +189,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
memoryRequest: this.saveArgs.workerMemoryRequest,
|
||||
memoryLimit: this.saveArgs.workerMemoryLimit
|
||||
},
|
||||
this._postgresModel.engineVersion,
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
session
|
||||
);
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars);
|
||||
/* TODO add second edit call for coordinator configuration
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
@@ -204,7 +199,6 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
memoryRequest: this.saveArgs.coordinatorMemoryRequest,
|
||||
memoryLimit: this.saveArgs.coordinatorMemoryLimit
|
||||
},
|
||||
this._postgresModel.engineVersion,
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
session
|
||||
);
|
||||
@@ -214,8 +208,6 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
// the edit wasn't successfully applied
|
||||
this.saveButton.enabled = true;
|
||||
throw err;
|
||||
} finally {
|
||||
session?.dispose();
|
||||
}
|
||||
try {
|
||||
await this._postgresModel.refresh();
|
||||
|
||||
@@ -39,8 +39,7 @@ export class PostgresCoordinatorNodeParametersPage extends PostgresParametersPag
|
||||
/* TODO add correct azdata call for editing coordinator parameters
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{ engineSettings: engineSettings },
|
||||
this._postgresModel.engineVersion,
|
||||
{ engineSettings: engineSettings.toString() },
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
session);
|
||||
*/
|
||||
@@ -51,7 +50,6 @@ export class PostgresCoordinatorNodeParametersPage extends PostgresParametersPag
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{ engineSettings: `''`, replaceEngineSettings: true },
|
||||
this._postgresModel.engineVersion,
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
session);
|
||||
*/
|
||||
@@ -62,7 +60,6 @@ export class PostgresCoordinatorNodeParametersPage extends PostgresParametersPag
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{ engineSettings: parameterName + '=' },
|
||||
this._postgresModel.engineVersion,
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
session);
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
// const coordinatorNodeParametersPage = new PostgresCoordinatorNodeParametersPage(modelView, 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 resourceHealthPage = new PostgresResourceHealthPage(modelView, this.dashboard, this._postgresModel);
|
||||
|
||||
|
||||
@@ -9,9 +9,10 @@ import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { PostgresModel } from '../../../models/postgresModel';
|
||||
import { ControllerModel } from '../../../models/controllerModel';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -50,9 +51,8 @@ export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage {
|
||||
|
||||
this.disposables.push(
|
||||
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_VERSION'] = this._postgresModel.engineVersion;
|
||||
vscode.commands.executeCommand('bookTreeView.openBook', this._context.asAbsolutePath('notebooks/arcDataServices'), true, 'postgres/tsg100-troubleshoot-postgres');
|
||||
}));
|
||||
|
||||
|
||||
@@ -217,21 +217,13 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
try {
|
||||
const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : '');
|
||||
if (password) {
|
||||
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
|
||||
try {
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{
|
||||
adminPassword: true,
|
||||
noWait: true
|
||||
},
|
||||
this._postgresModel.engineVersion,
|
||||
Object.assign({ 'AZDATA_PASSWORD': password }, this._controllerModel.azdataAdditionalEnvVars),
|
||||
session
|
||||
);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
{
|
||||
adminPassword: true,
|
||||
noWait: true
|
||||
},
|
||||
Object.assign({ 'AZDATA_PASSWORD': password }, this._controllerModel.azdataAdditionalEnvVars));
|
||||
vscode.window.showInformationMessage(loc.passwordReset);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -259,13 +251,7 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token) => {
|
||||
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
|
||||
try {
|
||||
return await this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name, this._controllerModel.azdataAdditionalEnvVars, session);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
|
||||
return await this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name, this._controllerModel.azdataAdditionalEnvVars, this._controllerModel.controllerContext);
|
||||
}
|
||||
);
|
||||
await this._controllerModel.refreshTreeNode();
|
||||
|
||||
@@ -152,12 +152,7 @@ export abstract class PostgresParametersPage extends DashboardPage {
|
||||
this.parameterUpdates.forEach((value, key) => {
|
||||
engineSettings.push(`${key}="${value}"`);
|
||||
});
|
||||
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
|
||||
try {
|
||||
await this.saveParameterEdits(engineSettings.toString(), session);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
await this.saveParameterEdits(engineSettings.toString());
|
||||
} catch (err) {
|
||||
// If an error occurs while editing the instance then re-enable the save button since
|
||||
// the edit wasn't successfully applied
|
||||
@@ -230,12 +225,7 @@ export abstract class PostgresParametersPage extends DashboardPage {
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
try {
|
||||
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
|
||||
try {
|
||||
await this.resetAllParameters(session);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
await this.resetAllParameters();
|
||||
} catch (err) {
|
||||
// If an error occurs while resetting the instance then re-enable the reset button since
|
||||
// the edit wasn't successfully applied
|
||||
@@ -423,12 +413,7 @@ export abstract class PostgresParametersPage extends DashboardPage {
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
const session = await this._postgresModel.controllerModel.acquireAzdataSession();
|
||||
try {
|
||||
await this.resetParameter(engineSetting.parameterName!, session);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
await this.resetParameter(engineSetting.parameterName!);
|
||||
try {
|
||||
await this._postgresModel.refresh();
|
||||
} 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>;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper } from '../../../constants';
|
||||
import { PostgresParametersPage } from './postgresParameters';
|
||||
@@ -35,34 +34,32 @@ export class PostgresWorkerNodeParametersPage extends PostgresParametersPage {
|
||||
return loc.nodeParametersDescription;
|
||||
}
|
||||
|
||||
|
||||
protected get engineSettings(): EngineSettingsModel[] {
|
||||
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(
|
||||
this._postgresModel.info.name,
|
||||
{ engineSettings: engineSettings },
|
||||
this._postgresModel.engineVersion,
|
||||
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(
|
||||
this._postgresModel.info.name,
|
||||
{ engineSettings: `''`, replaceEngineSettings: true },
|
||||
this._postgresModel.engineVersion,
|
||||
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(
|
||||
this._postgresModel.info.name,
|
||||
{ engineSettings: parameterName + '=' },
|
||||
this._postgresModel.engineVersion,
|
||||
this._postgresModel.controllerModel.azdataAdditionalEnvVars,
|
||||
session);
|
||||
this._postgresModel.controllerModel.controllerContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { InitializingComponent } from '../components/initializingComponent';
|
||||
import { AzureArcTreeDataProvider } from '../tree/azureArcTreeDataProvider';
|
||||
import { getErrorMessage } from '../../common/utils';
|
||||
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';
|
||||
|
||||
export type ConnectToControllerDialogModel = { controllerModel: ControllerModel, password: string };
|
||||
@@ -25,24 +25,34 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
protected modelBuilder!: azdata.ModelBuilder;
|
||||
protected dialog: azdata.window.Dialog;
|
||||
|
||||
protected urlInputBox!: azdata.InputBoxComponent;
|
||||
protected namespaceInputBox!: azdata.InputBoxComponent;
|
||||
protected kubeConfigInputBox!: FilePicker;
|
||||
protected clusterContextRadioGroup!: RadioOptionsGroup;
|
||||
protected nameInputBox!: azdata.InputBoxComponent;
|
||||
protected usernameInputBox!: azdata.InputBoxComponent;
|
||||
protected passwordInputBox!: azdata.InputBoxComponent;
|
||||
protected urlInputBox!: azdata.InputBoxComponent;
|
||||
|
||||
private _kubeClusters: KubeClusterContext[] = [];
|
||||
|
||||
protected dispose(): void {
|
||||
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; })[] {
|
||||
return [
|
||||
{
|
||||
component: this.namespaceInputBox,
|
||||
title: loc.namespace,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
component: this.urlInputBox,
|
||||
title: loc.controllerUrl,
|
||||
required: true
|
||||
layout: {
|
||||
info: loc.controllerUrlDescription
|
||||
}
|
||||
}, {
|
||||
component: this.kubeConfigInputBox.component(),
|
||||
title: loc.controllerKubeConfig,
|
||||
@@ -54,14 +64,17 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
}, {
|
||||
component: this.nameInputBox,
|
||||
title: loc.controllerName,
|
||||
required: false
|
||||
required: false,
|
||||
layout: {
|
||||
info: loc.controllerNameDescription
|
||||
}
|
||||
}, {
|
||||
component: this.usernameInputBox,
|
||||
title: loc.username,
|
||||
title: loc.controllerUsername,
|
||||
required: true
|
||||
}, {
|
||||
component: this.passwordInputBox,
|
||||
title: loc.password,
|
||||
title: loc.controllerPassword,
|
||||
required: true
|
||||
}
|
||||
];
|
||||
@@ -71,11 +84,14 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
protected readonlyFields(): azdata.Component[] { return []; }
|
||||
|
||||
protected initializeFields(controllerInfo: ControllerInfo | undefined, password: string | undefined) {
|
||||
this.namespaceInputBox = this.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
value: controllerInfo?.namespace,
|
||||
}).component();
|
||||
this.urlInputBox = this.modelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
value: controllerInfo?.url,
|
||||
// If we have a model then we're editing an existing connection so don't let them modify the URL
|
||||
readOnly: !!controllerInfo
|
||||
.withProps({
|
||||
value: controllerInfo?.endpoint,
|
||||
placeHolder: loc.controllerUrlPlaceholder,
|
||||
}).component();
|
||||
this.kubeConfigInputBox = new FilePicker(
|
||||
this.modelBuilder,
|
||||
@@ -83,22 +99,23 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
(disposable) => this._toDispose.push(disposable)
|
||||
);
|
||||
this.modelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
.withProps({
|
||||
value: controllerInfo?.kubeConfigFilePath || getDefaultKubeConfigPath()
|
||||
}).component();
|
||||
this.clusterContextRadioGroup = new RadioOptionsGroup(this.modelBuilder, (disposable) => this._toDispose.push(disposable));
|
||||
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.nameInputBox = this.modelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
.withProps({
|
||||
value: controllerInfo?.name
|
||||
}).component();
|
||||
this.usernameInputBox = this.modelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
.withProps({
|
||||
value: controllerInfo?.username
|
||||
}).component();
|
||||
this.passwordInputBox = this.modelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
.withProps({
|
||||
inputType: 'password',
|
||||
value: password
|
||||
}).component();
|
||||
@@ -114,15 +131,22 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
}
|
||||
|
||||
private loadRadioGroup(previousClusterContext?: string): void {
|
||||
this.clusterContextRadioGroup.load(async () => {
|
||||
const clusters = await getKubeConfigClusterContexts(this.kubeConfigInputBox.value!);
|
||||
this.clusterContextRadioGroup.load(() => {
|
||||
this._kubeClusters = getKubeConfigClusterContexts(this.kubeConfigInputBox.value!);
|
||||
const currentClusterContext = getCurrentClusterContext(this._kubeClusters, previousClusterContext, false);
|
||||
this.namespaceInputBox.value = currentClusterContext.namespace || this.namespaceInputBox.value;
|
||||
return {
|
||||
values: clusters.map(c => c.name),
|
||||
defaultValue: getCurrentClusterContext(clusters, previousClusterContext, false),
|
||||
values: this._kubeClusters.map(c => c.name),
|
||||
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 {
|
||||
this.id = controllerInfo?.id ?? uuid();
|
||||
this.resources = controllerInfo?.resources ?? [];
|
||||
@@ -168,7 +192,8 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
protected getControllerInfo(url: string, rememberPassword: boolean = false): ControllerInfo {
|
||||
return {
|
||||
id: this.id,
|
||||
url: url,
|
||||
endpoint: url || undefined,
|
||||
namespace: this.namespaceInputBox.value!,
|
||||
kubeConfigFilePath: this.kubeConfigInputBox.value!,
|
||||
kubeClusterContext: this.clusterContextRadioGroup.value!,
|
||||
name: this.nameInputBox.value ?? '',
|
||||
@@ -183,7 +208,7 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
|
||||
protected rememberPwCheckBox!: azdata.CheckBoxComponent;
|
||||
|
||||
protected fieldToFocusOn() {
|
||||
return this.urlInputBox;
|
||||
return this.namespaceInputBox;
|
||||
}
|
||||
|
||||
protected getComponents() {
|
||||
@@ -209,22 +234,25 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
let url = this.urlInputBox.value;
|
||||
// Only support https connections
|
||||
if (url.toLowerCase().startsWith('http://')) {
|
||||
url = url.replace('http', 'https');
|
||||
}
|
||||
// Append https if they didn't type it in
|
||||
if (!url.toLowerCase().startsWith('https://')) {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
// Append default port if one wasn't specified
|
||||
if (!/.*:\d*$/.test(url)) {
|
||||
url = `${url}:30080`;
|
||||
let url = this.urlInputBox.value || '';
|
||||
if (url) {
|
||||
// Only support https connections
|
||||
if (url.toLowerCase().startsWith('http://')) {
|
||||
url = url.replace('http', 'https');
|
||||
}
|
||||
// Append https if they didn't type it in
|
||||
if (!url.toLowerCase().startsWith('https://')) {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
// Append default port if one wasn't specified
|
||||
if (!/.*:\d*$/.test(url)) {
|
||||
url = `${url}:30080`;
|
||||
}
|
||||
}
|
||||
|
||||
const controllerInfo: ControllerInfo = this.getControllerInfo(url, !!this.rememberPwCheckBox.checked);
|
||||
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
|
||||
try {
|
||||
@@ -234,7 +262,7 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
|
||||
controllerModel.info.name = controllerModel.info.name || controllerModel.controllerConfig?.metadata.name || loc.defaultControllerName;
|
||||
} catch (err) {
|
||||
this.dialog.message = {
|
||||
text: loc.connectToControllerFailed(this.urlInputBox.value, err),
|
||||
text: loc.connectToControllerFailed(this.namespaceInputBox.value, err),
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
return false;
|
||||
@@ -267,11 +295,16 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
|
||||
if (!this.passwordInputBox.value) {
|
||||
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;
|
||||
try {
|
||||
await azdataApi.azdata.login(
|
||||
this.urlInputBox.value!,
|
||||
this.usernameInputBox.value!,
|
||||
{
|
||||
endpoint: controllerInfo.endpoint,
|
||||
namespace: controllerInfo.namespace
|
||||
},
|
||||
controllerInfo.username,
|
||||
this.passwordInputBox.value,
|
||||
{
|
||||
'KUBECONFIG': this.kubeConfigInputBox.value!,
|
||||
@@ -293,8 +326,6 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
|
||||
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 });
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ControllerModel } from '../../models/controllerModel';
|
||||
import { ControllerTreeNode } from './controllerTreeNode';
|
||||
import { TreeNode } from './treeNode';
|
||||
|
||||
const mementoToken = 'arcDataControllers';
|
||||
const mementoToken = 'arcDataControllers.v2';
|
||||
|
||||
/**
|
||||
* The TreeDataProvider for the Azure Arc view, which displays a list of registered
|
||||
|
||||
@@ -44,7 +44,7 @@ export class ControllerTreeNode extends TreeNode {
|
||||
} catch (err) {
|
||||
vscode.window.showErrorMessage(loc.errorConnectingToController(err));
|
||||
try {
|
||||
await this.model.refresh(false, true);
|
||||
await this.model.refresh(false);
|
||||
this.updateChildren(this.model.registrations);
|
||||
} catch (err) {
|
||||
if (!(err instanceof UserCancelledError)) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "azdata",
|
||||
"displayName": "%azdata.displayName%",
|
||||
"description": "%azdata.description%",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.2",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
|
||||
@@ -5,13 +5,26 @@
|
||||
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
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 { NoAzdataError } from './common/utils';
|
||||
import * as constants from './constants';
|
||||
import * as loc from './localizedConstants';
|
||||
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 {
|
||||
throwIfNoAzdata(azdata);
|
||||
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 {
|
||||
if (!localAzdata) {
|
||||
Logger.log(loc.noAzdata);
|
||||
@@ -55,47 +75,47 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
|
||||
profileName?: string,
|
||||
storageClass?: string,
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession) => {
|
||||
azdataContext?: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass, additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass, additionalEnvVars, azdataContext);
|
||||
},
|
||||
endpoint: {
|
||||
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.dc.endpoint.list(additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.dc.endpoint.list(additionalEnvVars, azdataContext);
|
||||
}
|
||||
},
|
||||
config: {
|
||||
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.dc.config.list(additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
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;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.dc.config.show(additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.dc.config.show(additionalEnvVars, azdataContext);
|
||||
}
|
||||
}
|
||||
},
|
||||
postgres: {
|
||||
server: {
|
||||
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.postgres.server.delete(name, additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
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;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.postgres.server.list(additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
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;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.postgres.server.show(name, additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.postgres.server.show(name, additionalEnvVars, azdataContext);
|
||||
},
|
||||
edit: async (
|
||||
name: string,
|
||||
@@ -112,31 +132,30 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
|
||||
replaceEngineSettings?: boolean;
|
||||
workers?: number;
|
||||
},
|
||||
engineVersion?: string,
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession) => {
|
||||
azdataContext?: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.postgres.server.edit(name, args, engineVersion, additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.postgres.server.edit(name, args, additionalEnvVars, azdataContext);
|
||||
}
|
||||
}
|
||||
},
|
||||
sql: {
|
||||
mi: {
|
||||
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession) => {
|
||||
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.sql.mi.delete(name, additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
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;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.sql.mi.list(additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
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;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.sql.mi.show(name, additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.arc.sql.mi.show(name, additionalEnvVars, azdataContext);
|
||||
},
|
||||
edit: async (
|
||||
name: string,
|
||||
@@ -148,11 +167,11 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
|
||||
noWait?: boolean;
|
||||
},
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession
|
||||
azdataContext?: string
|
||||
) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.sql.mi.edit(name, args, additionalEnvVars, session);
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
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);
|
||||
return azdataToolService.localAzdata.getPath();
|
||||
},
|
||||
login: async (endpoint: string, username: string, password: string, additionalEnvVars?: azdataExt.AdditionalEnvVars) => {
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.login(endpoint, username, password, additionalEnvVars);
|
||||
},
|
||||
acquireSession: async (endpoint: string, username: string, password: string, additionEnvVars?: azdataExt.AdditionalEnvVars) => {
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata?.acquireSession(endpoint, username, password, additionEnvVars);
|
||||
login: async (endpointOrNamespace: azdataExt.EndpointOrNamespace, username: string, password: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
|
||||
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata!.login(endpointOrNamespace, username, password, additionalEnvVars, azdataContext);
|
||||
},
|
||||
getSemVersion: async () => {
|
||||
await localAzdataDiscovered;
|
||||
|
||||
@@ -13,11 +13,15 @@ import { getPlatformDownloadLink, getPlatformReleaseVersion } from './azdataRele
|
||||
import { executeCommand, executeSudoCommand, ExitCodeError, ProcessOutput } from './common/childProcess';
|
||||
import { HttpClient } from './common/httpClient';
|
||||
import Logger from './common/logger';
|
||||
import { Deferred } from './common/promise';
|
||||
import { getErrorMessage, NoAzdataError, searchForCmd } from './common/utils';
|
||||
import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataInstallKey, azdataUpdateKey, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants';
|
||||
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 {
|
||||
dontPrompt = 'dontPrompt',
|
||||
prompt = 'prompt'
|
||||
@@ -32,20 +36,7 @@ export interface IAzdataTool extends azdataExt.IAzdataApi {
|
||||
* @param args The args to pass to azdata
|
||||
* @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>>
|
||||
}
|
||||
|
||||
class AzdataSession implements azdataExt.AzdataSession {
|
||||
|
||||
private _session = new Deferred<void>();
|
||||
|
||||
public sessionEnded(): Promise<void> {
|
||||
return this._session.promise;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._session.resolve();
|
||||
}
|
||||
executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<R>>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,9 +45,6 @@ class AzdataSession implements azdataExt.AzdataSession {
|
||||
export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
|
||||
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) {
|
||||
this._semVersion = new SemVer(version);
|
||||
@@ -90,7 +78,7 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
profileName?: string,
|
||||
storageClass?: string,
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
const args = ['arc', 'dc', 'create',
|
||||
'--namespace', namespace,
|
||||
'--name', name,
|
||||
@@ -104,32 +92,32 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
if (storageClass) {
|
||||
args.push('--storage-class', storageClass);
|
||||
}
|
||||
return this.executeCommand<void>(args, additionalEnvVars, session);
|
||||
return this.executeCommand<void>(args, additionalEnvVars, azdataContext);
|
||||
},
|
||||
endpoint: {
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.DcEndpointListResult[]>(['arc', 'dc', 'endpoint', 'list'], additionalEnvVars, session);
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.DcEndpointListResult[]>(['arc', 'dc', 'endpoint', 'list'], additionalEnvVars, azdataContext);
|
||||
}
|
||||
},
|
||||
config: {
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.DcConfigListResult[]>(['arc', 'dc', 'config', 'list'], additionalEnvVars, session);
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.DcConfigListResult[]>(['arc', 'dc', 'config', 'list'], additionalEnvVars, azdataContext);
|
||||
},
|
||||
show: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> => {
|
||||
return this.executeCommand<azdataExt.DcConfigShowResult>(['arc', 'dc', 'config', 'show'], additionalEnvVars, session);
|
||||
show: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> => {
|
||||
return this.executeCommand<azdataExt.DcConfigShowResult>(['arc', 'dc', 'config', 'show'], additionalEnvVars, azdataContext);
|
||||
}
|
||||
}
|
||||
},
|
||||
postgres: {
|
||||
server: {
|
||||
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
return this.executeCommand<void>(['arc', 'postgres', 'server', 'delete', '-n', name, '--force'], additionalEnvVars, session);
|
||||
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
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[]>> => {
|
||||
return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list'], additionalEnvVars, session);
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> => {
|
||||
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>> => {
|
||||
return this.executeCommand<azdataExt.PostgresServerShowResult>(['arc', 'postgres', 'server', 'show', '-n', name], additionalEnvVars, session);
|
||||
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, azdataContext);
|
||||
},
|
||||
edit: (
|
||||
name: string,
|
||||
@@ -146,9 +134,8 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
replaceEngineSettings?: boolean,
|
||||
workers?: number
|
||||
},
|
||||
engineVersion?: string,
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name];
|
||||
if (args.adminPassword) { argsArray.push('--admin-password'); }
|
||||
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.replaceEngineSettings) { argsArray.push('--replace-engine-settings'); }
|
||||
if (args.workers) { argsArray.push('--workers', args.workers.toString()); }
|
||||
if (engineVersion) { argsArray.push('--engine-version', engineVersion); }
|
||||
return this.executeCommand<void>(argsArray, additionalEnvVars, session);
|
||||
return this.executeCommand<void>(argsArray, additionalEnvVars, azdataContext);
|
||||
}
|
||||
}
|
||||
},
|
||||
sql: {
|
||||
mi: {
|
||||
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
return this.executeCommand<void>(['arc', 'sql', 'mi', 'delete', '-n', name], additionalEnvVars, session);
|
||||
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
return this.executeCommand<void>(['arc', 'sql', 'mi', 'delete', '-n', name], additionalEnvVars, azdataContext);
|
||||
},
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> => {
|
||||
return this.executeCommand<azdataExt.SqlMiListResult[]>(['arc', 'sql', 'mi', 'list'], additionalEnvVars, session);
|
||||
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> => {
|
||||
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>> => {
|
||||
return this.executeCommand<azdataExt.SqlMiShowResult>(['arc', 'sql', 'mi', 'show', '-n', name], additionalEnvVars, session);
|
||||
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, azdataContext);
|
||||
},
|
||||
edit: (
|
||||
name: string,
|
||||
@@ -186,8 +172,7 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
memoryRequest?: string,
|
||||
noWait?: boolean,
|
||||
},
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars,
|
||||
session?: azdataExt.AzdataSession
|
||||
additionalEnvVars?: azdataExt.AdditionalEnvVars
|
||||
): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
const argsArray = ['arc', 'sql', 'mi', 'edit', '-n', name];
|
||||
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.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
|
||||
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>> {
|
||||
// Since login changes the context we want to wait until all currently executing commands are finished before this is executed
|
||||
while (this._currentlyExecutingCommands.length > 0) {
|
||||
await this._currentlyExecutingCommands[0];
|
||||
}
|
||||
// Logins need to be done outside the session aware logic so call impl directly
|
||||
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;
|
||||
public async login(endpointOrNamespace: azdataExt.EndpointOrNamespace, username: string, password: string, additionalEnvVars: azdataExt.AdditionalEnvVars = {}, azdataContext?: string): Promise<azdataExt.AzdataOutput<void>> {
|
||||
const args = ['login', '-u', username];
|
||||
if (endpointOrNamespace.endpoint) {
|
||||
args.push('-e', endpointOrNamespace.endpoint);
|
||||
} else if (endpointOrNamespace.namespace) {
|
||||
args.push('--namespace', endpointOrNamespace.namespace);
|
||||
} 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
|
||||
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;
|
||||
throw new Error(loc.endpointOrNamespaceRequired);
|
||||
}
|
||||
|
||||
await this.login(endpoint, username, password, additionalEnvVars);
|
||||
return session;
|
||||
return this.executeCommand<void>(args, Object.assign({}, additionalEnvVars, { 'AZDATA_PASSWORD': password }), azdataContext);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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 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 {
|
||||
if (azdataContext) {
|
||||
args = args.concat('--controller-context', azdataContext);
|
||||
}
|
||||
const output = JSON.parse((await executeAzdataCommand(`"${this._path}"`, args.concat(['--output', 'json']), additionalEnvVars)).stdout);
|
||||
return {
|
||||
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> {
|
||||
if (currentAzdata !== undefined) {
|
||||
const newSemVersion = await discoverLatestAvailableAzdataVersion();
|
||||
if (newSemVersion.compare(await currentAzdata.getSemVersion()) === 1) {
|
||||
Logger.log(loc.foundAzdataVersionToUpdateTo(newSemVersion.raw, (await currentAzdata.getSemVersion()).raw));
|
||||
const currentSemVersion = await currentAzdata.getSemVersion();
|
||||
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);
|
||||
} else {
|
||||
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 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.
|
||||
* @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> {
|
||||
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);
|
||||
async function promptToUpdateAzdata(newVersion: string, userRequested: boolean = false, required = false): Promise<boolean> {
|
||||
if (required) {
|
||||
let response: string | undefined = loc.yes;
|
||||
|
||||
const responses = [loc.yes, loc.no];
|
||||
Logger.log(loc.promptForRequiredAzdataUpdateLog(MIN_AZDATA_VERSION.raw, newVersion));
|
||||
response = await vscode.window.showInformationMessage(loc.promptForRequiredAzdataUpdate(MIN_AZDATA_VERSION.raw, 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));
|
||||
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));
|
||||
}
|
||||
}
|
||||
} 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 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 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 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 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);
|
||||
@@ -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 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 endpointOrNamespaceRequired = localize('azdata.endpointOrNamespaceRequired', "Either an endpoint or a namespace must be specified");
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('api', function (): void {
|
||||
it('succeed when azdata present and EULA accepted', async function (): Promise<void> {
|
||||
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
|
||||
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();
|
||||
azdataToolService.localAzdata = azdataTool;
|
||||
// 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) => {
|
||||
// Version needs to be valid so it can be parsed correctly
|
||||
if (args[0] === '--version') {
|
||||
return { stdout: `1.0.0`, stderr: '' };
|
||||
return { stdout: `99.0.0`, stderr: '' };
|
||||
}
|
||||
console.log(args[0]);
|
||||
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> {
|
||||
await assertCallback(api.azdata.getPath(), 'getPath');
|
||||
await assertCallback(api.azdata.getSemVersion(), 'getSemVersion');
|
||||
await assertCallback(api.azdata.login('', '', ''), 'login');
|
||||
await assertCallback((async () => {
|
||||
let session: azdataExt.AzdataSession | undefined;
|
||||
try {
|
||||
session = await api.azdata.acquireSession('', '', '');
|
||||
} finally {
|
||||
session?.dispose();
|
||||
}
|
||||
})(), 'acquireSession');
|
||||
await assertCallback(api.azdata.login({ endpoint: 'https://127.0.0.1' }, '', ''), 'login');
|
||||
await assertCallback(api.azdata.login({ namespace: 'namespace' }, '', ''), 'login');
|
||||
await assertCallback(api.azdata.version(), 'version');
|
||||
|
||||
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.delete(''), 'arc sql mi delete');
|
||||
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.delete(''), 'arc sql postgres server delete');
|
||||
await assertCallback(api.azdata.arc.postgres.server.show(''), 'arc sql postgres server show');
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* 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 sinon from 'sinon';
|
||||
import * as vscode from 'vscode';
|
||||
@@ -17,9 +16,8 @@ import * as fs from 'fs';
|
||||
import { AzdataReleaseInfo } from '../azdataReleaseInfo';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
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');
|
||||
|
||||
/**
|
||||
@@ -222,120 +220,10 @@ describe('azdata', function () {
|
||||
const endpoint = 'myEndpoint';
|
||||
const username = 'myUsername';
|
||||
const password = 'myPassword';
|
||||
await azdataTool.login(endpoint, username, password);
|
||||
await azdataTool.login({ endpoint: endpoint }, username, password);
|
||||
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> {
|
||||
executeCommandStub.resolves({ stdout: '1.0.0', stderr: '' });
|
||||
await azdataTool.version();
|
||||
@@ -777,7 +665,7 @@ async function testDarwinSkippedUpdateDontPrompt() {
|
||||
async function testWin32SkippedUpdateDontPrompt() {
|
||||
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
|
||||
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() {
|
||||
|
||||
@@ -18,7 +18,3 @@ export async function assertRejected(promise: Promise<any>, message: string): Pr
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
export async function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
|
||||
47
extensions/azdata/src/typings/azdata-ext.d.ts
vendored
47
extensions/azdata/src/typings/azdata-ext.d.ts
vendored
@@ -160,7 +160,7 @@ declare module 'azdata-ext' {
|
||||
|
||||
export interface PostgresServerShowResult {
|
||||
apiVersion: string, // "arcdata.microsoft.com/v1alpha1"
|
||||
kind: string, // "postgresql-12"
|
||||
kind: string, // "postgresql"
|
||||
metadata: {
|
||||
creationTimestamp: string, // "2020-08-19T20:25:11Z"
|
||||
generation: number, // 1
|
||||
@@ -177,7 +177,8 @@ declare module 'azdata-ext' {
|
||||
}[],
|
||||
settings: {
|
||||
default: { [key: string]: string } // { "max_connections": "101", "work_mem": "4MB" }
|
||||
}
|
||||
},
|
||||
version: string // "12"
|
||||
},
|
||||
scale: {
|
||||
shards: number, // 1 (shards was renamed to workers, kept here for backwards compatibility)
|
||||
@@ -244,25 +245,27 @@ declare module 'azdata-ext' {
|
||||
code?: number
|
||||
}
|
||||
|
||||
export interface AzdataSession extends vscode.Disposable { }
|
||||
|
||||
export interface EndpointOrNamespace {
|
||||
endpoint?: string,
|
||||
namespace?: string
|
||||
}
|
||||
export interface IAzdataApi {
|
||||
arc: {
|
||||
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: {
|
||||
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcEndpointListResult[]>>
|
||||
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcEndpointListResult[]>>
|
||||
},
|
||||
config: {
|
||||
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcConfigListResult[]>>,
|
||||
show(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<DcConfigShowResult>>
|
||||
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcConfigListResult[]>>,
|
||||
show(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcConfigShowResult>>
|
||||
}
|
||||
},
|
||||
postgres: {
|
||||
server: {
|
||||
delete(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<void>>,
|
||||
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<PostgresServerListResult[]>>,
|
||||
show(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<PostgresServerShowResult>>,
|
||||
delete(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
|
||||
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<PostgresServerListResult[]>>,
|
||||
show(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<PostgresServerShowResult>>,
|
||||
edit(
|
||||
name: string,
|
||||
args: {
|
||||
@@ -278,17 +281,16 @@ declare module 'azdata-ext' {
|
||||
replaceEngineSettings?: boolean,
|
||||
workers?: number
|
||||
},
|
||||
engineVersion?: string,
|
||||
additionalEnvVars?: AdditionalEnvVars,
|
||||
session?: AzdataSession
|
||||
azdataContext?: string
|
||||
): Promise<AzdataOutput<void>>
|
||||
}
|
||||
},
|
||||
sql: {
|
||||
mi: {
|
||||
delete(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<void>>,
|
||||
list(additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<SqlMiListResult[]>>,
|
||||
show(name: string, additionalEnvVars?: AdditionalEnvVars, session?: AzdataSession): Promise<AzdataOutput<SqlMiShowResult>>,
|
||||
delete(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
|
||||
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<SqlMiListResult[]>>,
|
||||
show(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<SqlMiShowResult>>,
|
||||
edit(
|
||||
name: string,
|
||||
args: {
|
||||
@@ -299,22 +301,13 @@ declare module 'azdata-ext' {
|
||||
noWait?: boolean,
|
||||
},
|
||||
additionalEnvVars?: AdditionalEnvVars,
|
||||
session?: AzdataSession
|
||||
azdataContext?: string
|
||||
): Promise<AzdataOutput<void>>
|
||||
}
|
||||
}
|
||||
},
|
||||
getPath(): Promise<string>,
|
||||
login(endpoint: string, username: string, password: string, additionalEnvVars?: AdditionalEnvVars): 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>,
|
||||
login(endpointOrNamespace: EndpointOrNamespace, username: string, password: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
},
|
||||
{
|
||||
"name": "azdata-old",
|
||||
"version": "20.3.1"
|
||||
"version": "20.3.2"
|
||||
}
|
||||
],
|
||||
"when": "target=new-aks&&version=bdc2019"
|
||||
@@ -266,7 +266,7 @@
|
||||
},
|
||||
{
|
||||
"name": "azdata-old",
|
||||
"version": "20.3.1"
|
||||
"version": "20.3.2"
|
||||
}
|
||||
],
|
||||
"when": "target=existing-aks&&version=bdc2019"
|
||||
@@ -284,7 +284,7 @@
|
||||
},
|
||||
{
|
||||
"name": "azdata-old",
|
||||
"version": "20.3.1"
|
||||
"version": "20.3.2"
|
||||
}
|
||||
],
|
||||
"when": "target=existing-kubeadm&&version=bdc2019"
|
||||
@@ -302,7 +302,7 @@
|
||||
},
|
||||
{
|
||||
"name": "azdata-old",
|
||||
"version": "20.3.1"
|
||||
"version": "20.3.2"
|
||||
}
|
||||
],
|
||||
"when": "target=existing-aro&&version=bdc2019"
|
||||
@@ -320,7 +320,7 @@
|
||||
},
|
||||
{
|
||||
"name": "azdata-old",
|
||||
"version": "20.3.1"
|
||||
"version": "20.3.2"
|
||||
}
|
||||
],
|
||||
"when": "target=existing-openshift&&version=bdc2019"
|
||||
|
||||
3
extensions/data-workspace/images/refresh.svg
Normal file
3
extensions/data-workspace/images/refresh.svg
Normal 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 |
@@ -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 fileDoesNotExist = (name: string): string => { return localize('fileDoesNotExist', "File '{0}' doesn't exist", name); };
|
||||
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
|
||||
export const projectsConfigurationKey = 'projects';
|
||||
@@ -75,6 +76,9 @@ export const LocalClonePathPlaceholder = localize('dataworkspace.localClonePathP
|
||||
export const ProjectConfigurationKey = 'projects';
|
||||
export const ProjectSaveLocationKey = 'defaultProjectSaveLocation';
|
||||
|
||||
// Dashboard dialog
|
||||
export const Refresh = localize('dataworksapce.refresh', 'Refresh');
|
||||
|
||||
export namespace cssStyles {
|
||||
export const title = { 'font-size': '18px', 'font-weight': '600' };
|
||||
export const tableHeader = { 'text-align': 'left', 'font-weight': '500', 'font-size': '13px', 'user-select': 'text' };
|
||||
|
||||
@@ -13,11 +13,13 @@ export interface IconPath {
|
||||
export class IconPathHelper {
|
||||
private static extensionContext: vscode.ExtensionContext;
|
||||
public static folder: IconPath;
|
||||
public static refresh: IconPath;
|
||||
|
||||
public static setExtensionContext(extensionContext: vscode.ExtensionContext) {
|
||||
IconPathHelper.extensionContext = extensionContext;
|
||||
|
||||
IconPathHelper.folder = IconPathHelper.makeIcon('folder', true);
|
||||
IconPathHelper.refresh = IconPathHelper.makeIcon('refresh', true);
|
||||
}
|
||||
|
||||
private static makeIcon(name: string, sameIcon: boolean = false) {
|
||||
|
||||
10
extensions/data-workspace/src/dataworkspace.d.ts
vendored
10
extensions/data-workspace/src/dataworkspace.d.ts
vendored
@@ -67,6 +67,11 @@ declare module 'dataworkspace' {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@@ -77,11 +82,6 @@ declare module 'dataworkspace' {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,7 @@ import { IDashboardColumnInfo, IDashboardTable, IProjectAction, IProjectActionGr
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import * as constants from '../common/constants';
|
||||
import { IconPathHelper } from '../common/iconHelper';
|
||||
import { IWorkspaceService } from '../common/interfaces';
|
||||
import { fileExist } from '../common/utils';
|
||||
|
||||
@@ -17,6 +18,8 @@ export class ProjectDashboard {
|
||||
private modelView: azdata.ModelView | undefined;
|
||||
private projectProvider: IProjectProvider | undefined;
|
||||
private overviewTab: azdata.DashboardTab | undefined;
|
||||
private rootContainer: azdata.FlexContainer | undefined;
|
||||
private tableContainer: azdata.Component | undefined;
|
||||
|
||||
constructor(private _workspaceService: IWorkspaceService, private _treeItem: WorkspaceTreeItem) {
|
||||
}
|
||||
@@ -41,7 +44,7 @@ export class ProjectDashboard {
|
||||
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.registerTabs(async (modelView: azdata.ModelView) => {
|
||||
this.modelView = modelView;
|
||||
@@ -49,8 +52,8 @@ export class ProjectDashboard {
|
||||
this.overviewTab = {
|
||||
title: '',
|
||||
id: 'overview-tab',
|
||||
content: this.createContainer(title, location),
|
||||
toolbar: this.createToolbarContainer()
|
||||
content: this.createContainer(title, projectFilePath),
|
||||
toolbar: this.createToolbarContainer(projectFilePath)
|
||||
};
|
||||
return [
|
||||
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;
|
||||
|
||||
// Add actions as buttons
|
||||
@@ -69,17 +72,32 @@ export class ProjectDashboard {
|
||||
projectActions.forEach((action, actionIndex) => {
|
||||
if (this.isProjectAction(action)) {
|
||||
const button = this.createButton(action);
|
||||
buttons.push({ component: button });
|
||||
buttons.push({ component: button, toolbarSeparatorAfter: (projectActionsLength - 1 === actionIndex) });
|
||||
} else {
|
||||
const groupLength = action.actions.length;
|
||||
|
||||
action.actions.forEach((groupAction, index) => {
|
||||
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()
|
||||
.withToolbarItems(
|
||||
buttons
|
||||
@@ -105,21 +123,21 @@ export class ProjectDashboard {
|
||||
return button;
|
||||
}
|
||||
|
||||
private createContainer(title: string, location: string): azdata.FlexContainer {
|
||||
const rootContainer = this.modelView!.modelBuilder.flexContainer().withLayout(
|
||||
private createContainer(title: string, projectFilePath: string): azdata.FlexContainer {
|
||||
this.rootContainer = this.modelView!.modelBuilder.flexContainer().withLayout(
|
||||
{
|
||||
flexFlow: 'column',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}).component();
|
||||
|
||||
const headerContainer = this.createHeader(title, location);
|
||||
const tableContainer = this.createTables();
|
||||
const headerContainer = this.createHeader(title, projectFilePath);
|
||||
this.tableContainer = this.createTables(projectFilePath);
|
||||
|
||||
rootContainer.addItem(headerContainer);
|
||||
rootContainer.addItem(tableContainer);
|
||||
this.rootContainer.addItem(headerContainer);
|
||||
this.rootContainer.addItem(this.tableContainer);
|
||||
|
||||
return rootContainer;
|
||||
return this.rootContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,8 +188,8 @@ export class ProjectDashboard {
|
||||
/**
|
||||
* Adds all the tables to the container
|
||||
*/
|
||||
private createTables(): azdata.Component {
|
||||
const dashboardData: IDashboardTable[] = this.projectProvider!.dashboardComponents;
|
||||
private createTables(projectFile: string): azdata.Component {
|
||||
const dashboardData: IDashboardTable[] = this.projectProvider!.getDashboardComponents(projectFile);
|
||||
|
||||
const tableContainer = this.modelView!.modelBuilder.flexContainer().withLayout(
|
||||
{
|
||||
@@ -186,54 +204,62 @@ export class ProjectDashboard {
|
||||
.component();
|
||||
tableContainer.addItem(tableNameLabel, { CSSStyles: { 'padding-left': '25px', 'padding-bottom': '20px', ...constants.cssStyles.title } });
|
||||
|
||||
const columns: azdata.DeclarativeTableColumn[] = [];
|
||||
info.columns.forEach((column: IDashboardColumnInfo) => {
|
||||
let col = {
|
||||
displayName: column.displayName,
|
||||
valueType: column.type === 'icon' ? azdata.DeclarativeDataType.component : azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: column.width,
|
||||
headerCssStyles: {
|
||||
'border': 'none',
|
||||
...constants.cssStyles.tableHeader
|
||||
},
|
||||
rowCssStyles: {
|
||||
...constants.cssStyles.tableRow
|
||||
},
|
||||
};
|
||||
columns.push(col);
|
||||
});
|
||||
|
||||
const data: azdata.DeclarativeTableCellValue[][] = [];
|
||||
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();
|
||||
|
||||
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 });
|
||||
}
|
||||
if (info.data.length === 0) {
|
||||
const noDataText = constants.noPreviousData(info.name.toLocaleLowerCase());
|
||||
const noDataLabel = this.modelView!.modelBuilder.text()
|
||||
.withProperties<azdata.TextComponentProperties>({ value: noDataText })
|
||||
.component();
|
||||
tableContainer.addItem(noDataLabel, { CSSStyles: { 'padding-left': '25px', 'padding-bottom': '20px' } });
|
||||
} else {
|
||||
const columns: azdata.DeclarativeTableColumn[] = [];
|
||||
info.columns.forEach((column: IDashboardColumnInfo) => {
|
||||
let col = {
|
||||
displayName: column.displayName,
|
||||
valueType: column.type === 'icon' ? azdata.DeclarativeDataType.component : azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: column.width,
|
||||
headerCssStyles: {
|
||||
'border': 'none',
|
||||
...constants.cssStyles.tableHeader
|
||||
},
|
||||
rowCssStyles: {
|
||||
...constants.cssStyles.tableRow
|
||||
},
|
||||
};
|
||||
columns.push(col);
|
||||
});
|
||||
data.push(columnValue);
|
||||
});
|
||||
|
||||
const table = this.modelView!.modelBuilder.declarativeTable()
|
||||
.withProperties<azdata.DeclarativeTableProperties>({ columns: columns, dataValues: data, ariaLabel: info.name, CSSStyles: { 'margin-left': '30px' } }).component();
|
||||
const data: azdata.DeclarativeTableCellValue[][] = [];
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,9 @@ export function createProjectProvider(projectTypes: IProjectType[], projectActio
|
||||
return Promise.resolve(location);
|
||||
},
|
||||
projectActions: projectActions,
|
||||
dashboardComponents: dashboardComponents
|
||||
getDashboardComponents: (projectFile: string): IDashboardTable[] => {
|
||||
return dashboardComponents;
|
||||
}
|
||||
};
|
||||
return projectProvider;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 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 * as should from 'should';
|
||||
import * as sinon from 'sinon';
|
||||
@@ -111,7 +111,8 @@ suite('workspaceTreeDataProvider Tests', function (): void {
|
||||
id: 'Target Version',
|
||||
run: async (): Promise<any> => { return Promise.resolve(); }
|
||||
}],
|
||||
dashboardComponents: [{
|
||||
getDashboardComponents: (projectFile: string): IDashboardTable[] => {
|
||||
return [{
|
||||
name: 'Deployments',
|
||||
columns: [{ displayName: 'c1', width: 75, type: 'string' }],
|
||||
data: [['d1']]
|
||||
@@ -120,8 +121,8 @@ suite('workspaceTreeDataProvider Tests', function (): void {
|
||||
name: 'Builds',
|
||||
columns: [{ displayName: 'c1', width: 75, type: 'string' }],
|
||||
data: [['d1']]
|
||||
}]
|
||||
};
|
||||
}];
|
||||
}};
|
||||
const getProjectProviderStub = sinon.stub(workspaceService, 'getProjectProvider');
|
||||
getProjectProviderStub.onFirstCall().resolves(undefined);
|
||||
getProjectProviderStub.onSecondCall().resolves(projectProvider);
|
||||
|
||||
@@ -11,20 +11,21 @@ export async function deactivate(): Promise<any> {
|
||||
}
|
||||
|
||||
export async function activate(context: ExtensionContext): Promise<void> {
|
||||
context.subscriptions.push(commands.registerCommand('git.credential', async (data: any) => {
|
||||
try {
|
||||
const { stdout, stderr } = await exec(`git credential ${data.command}`, {
|
||||
stdin: data.stdin,
|
||||
env: Object.assign(process.env, { GIT_TERMINAL_PROMPT: '0' })
|
||||
});
|
||||
return { stdout, stderr, code: 0 };
|
||||
} catch ({ stdout, stderr, error }) {
|
||||
const code = error.code || 0;
|
||||
if (stderr.indexOf('terminal prompts disabled') !== -1) {
|
||||
stderr = '';
|
||||
}
|
||||
return { stdout, stderr, code };
|
||||
}
|
||||
context.subscriptions.push(commands.registerCommand('git.credential', async (_data: any) => {
|
||||
return { stdout: '', stderr: '', code: 0 };
|
||||
// try {
|
||||
// const { stdout, stderr } = await exec(`git credential ${data.command}`, {
|
||||
// stdin: data.stdin,
|
||||
// env: Object.assign(process.env, { GIT_TERMINAL_PROMPT: '0' })
|
||||
// });
|
||||
// return { stdout, stderr, code: 0 };
|
||||
// } catch ({ stdout, stderr, error }) {
|
||||
// const code = error.code || 0;
|
||||
// if (stderr.indexOf('terminal prompts disabled') !== -1) {
|
||||
// stderr = '';
|
||||
// }
|
||||
// return { stdout, stderr, code };
|
||||
// }
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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": {
|
||||
"Windows_86": "win-x86-netcoreapp3.1.zip",
|
||||
"Windows_64": "win-x64-netcoreapp3.1.zip",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kusto",
|
||||
"version": "0.5.2",
|
||||
"version": "0.5.3",
|
||||
"publisher": "Microsoft",
|
||||
"aiKey": "AIF-444c3af9-8e69-4462-ab49-4191e6ad1916",
|
||||
"activationEvents": [
|
||||
@@ -8,7 +8,7 @@
|
||||
],
|
||||
"engines": {
|
||||
"vscode": "*",
|
||||
"azdata": ">=1.27.0"
|
||||
"azdata": ">=1.28.0"
|
||||
},
|
||||
"main": "./out/main",
|
||||
"repository": {
|
||||
|
||||
@@ -56,6 +56,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
||||
this.bookTocManager = new BookTocManager();
|
||||
this._bookViewer = vscode.window.createTreeView(this.viewId, { showCollapseAll: true, treeDataProvider: this });
|
||||
this._bookViewer.onDidChangeVisibility(async e => {
|
||||
await this.initialized;
|
||||
// Whenever the viewer changes visibility then try and reveal the currently active document
|
||||
// in the tree view
|
||||
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);
|
||||
} else {
|
||||
vscode.window.showInformationMessage(loc.msgBookAlreadyTrusted);
|
||||
@@ -134,7 +135,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
||||
let bookPathToUpdate = bookTreeItem.book?.contentPath;
|
||||
if (bookPathToUpdate) {
|
||||
let pinStatusChanged = await this.bookPinManager.pinNotebook(bookTreeItem);
|
||||
TelemetryReporter.createActionEvent(BookTelemetryView, NbTelemetryActions.PinNotebook).send();
|
||||
TelemetryReporter.sendActionEvent(BookTelemetryView, NbTelemetryActions.PinNotebook);
|
||||
if (pinStatusChanged) {
|
||||
bookTreeItem.contextValue = 'pinnedNotebook';
|
||||
}
|
||||
@@ -154,7 +155,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
||||
async createBook(): Promise<void> {
|
||||
const dialog = new CreateBookDialog(this.bookTocManager);
|
||||
dialog.createDialog();
|
||||
TelemetryReporter.createActionEvent(BookTelemetryView, NbTelemetryActions.CreateBook).send();
|
||||
TelemetryReporter.sendActionEvent(BookTelemetryView, NbTelemetryActions.CreateBook);
|
||||
}
|
||||
|
||||
async getSelectionQuickPick(movingElement: BookTreeItem): Promise<quickPickResults> {
|
||||
@@ -207,6 +208,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
||||
}
|
||||
|
||||
async editBook(movingElement: BookTreeItem): Promise<void> {
|
||||
TelemetryReporter.sendActionEvent(BookTelemetryView, NbTelemetryActions.MoveNotebook);
|
||||
const selectionResults = await this.getSelectionQuickPick(movingElement);
|
||||
if (selectionResults) {
|
||||
const pickedSection = selectionResults.quickPickSection;
|
||||
@@ -238,7 +240,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
||||
await this.showPreviewFile(urlToOpen);
|
||||
}
|
||||
|
||||
TelemetryReporter.createActionEvent(BookTelemetryView, NbTelemetryActions.OpenBook).send();
|
||||
TelemetryReporter.sendActionEvent(BookTelemetryView, NbTelemetryActions.OpenBook);
|
||||
} catch (e) {
|
||||
// if there is an error remove book from context
|
||||
const index = this.books.findIndex(book => book.bookPath === bookPath);
|
||||
@@ -298,7 +300,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
||||
}
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
}
|
||||
TelemetryReporter.createActionEvent(BookTelemetryView, NbTelemetryActions.CloseBook).send();
|
||||
TelemetryReporter.sendActionEvent(BookTelemetryView, NbTelemetryActions.CloseBook);
|
||||
} catch (e) {
|
||||
vscode.window.showErrorMessage(loc.closeBookError(book.root, e instanceof Error ? e.message : e));
|
||||
} finally {
|
||||
@@ -383,7 +385,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
||||
this._visitedNotebooks = this._visitedNotebooks.concat([normalizedResource]);
|
||||
}
|
||||
}
|
||||
TelemetryReporter.createActionEvent(BookTelemetryView, NbTelemetryActions.OpenNotebookFromBook);
|
||||
TelemetryReporter.sendActionEvent(BookTelemetryView, NbTelemetryActions.OpenNotebookFromBook);
|
||||
} catch (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) {
|
||||
let openDocument = azdata.nb.activeNotebookEditor;
|
||||
if (openDocument) {
|
||||
notebookPath = openDocument.document.uri.fsPath.replace(/\\/g, '/');
|
||||
notebookPath = openDocument.document.uri.fsPath;
|
||||
}
|
||||
} else if (uri.fsPath) {
|
||||
notebookPath = uri.fsPath.replace(/\\/g, '/');
|
||||
notebookPath = uri.fsPath;
|
||||
}
|
||||
|
||||
if (shouldReveal || this._bookViewer?.visible) {
|
||||
@@ -414,6 +416,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
||||
}
|
||||
|
||||
async findAndExpandParentNode(notebookPath: string): Promise<BookTreeItem | undefined> {
|
||||
notebookPath = notebookPath.replace(/\\/g, '/');
|
||||
const parentBook = this.books.find(b => notebookPath.indexOf(b.bookPath) > -1);
|
||||
if (!parentBook) {
|
||||
// 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
|
||||
// get the children of root node and expand the nodes to the notebook level.
|
||||
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)
|
||||
const parentPath = notebookPath.substring(0, notebookPath.lastIndexOf(path.posix.sep));
|
||||
// The path to the Notebook we're looking for (these are the nodes we're looking to expand)
|
||||
const notebookFolders = notebookPath.split('/');
|
||||
// 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
|
||||
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
|
||||
// we find the one for our Notebook
|
||||
while (depthOfNotebookInBook > 0) {
|
||||
while (depthOfNotebookInBook > -1) {
|
||||
// check if the notebook is available in already expanded levels.
|
||||
bookItem = parentBook.bookItems.find(b => b.tooltip === notebookPath);
|
||||
if (bookItem) {
|
||||
return bookItem;
|
||||
}
|
||||
// Search for the parent item
|
||||
// notebook can be inside the same folder as parent and can be in a different folder as well
|
||||
// so check for both scenarios.
|
||||
let bookItemToExpand = parentBook.bookItems.find(b => b.tooltip.indexOf(parentPath) > -1) ??
|
||||
// Walk down from the top level parent folder one level at each iteration
|
||||
// and keep expanding until we reach the target notebook leaf
|
||||
let parentBookPath: string = notebookFolders.slice(0, notebookFolders.length - depthOfNotebookInBook).join('/');
|
||||
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);
|
||||
if (!bookItemToExpand) {
|
||||
break;
|
||||
@@ -456,7 +459,13 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
||||
// continue expanding and search its children
|
||||
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--;
|
||||
}
|
||||
return bookItem;
|
||||
|
||||
@@ -29,7 +29,7 @@ export class BookTrustManager implements IBookTrustManager {
|
||||
let hasTrustedBookPath: boolean = treeBookItems
|
||||
.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)));
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export enum NbTelemetryActions {
|
||||
SaveBook = 'BookSaved',
|
||||
CreateBook = 'BookCreated',
|
||||
PinNotebook = 'NotebookPinned',
|
||||
OpenNotebookFromBook = 'NotebookOpenedFromBook'
|
||||
OpenNotebookFromBook = 'NotebookOpenedFromBook',
|
||||
MoveNotebook = 'MoveNotebook',
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
should(parent).be.undefined();
|
||||
|
||||
@@ -243,8 +243,9 @@ describe('BooksTreeViewTests', function () {
|
||||
});
|
||||
|
||||
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);
|
||||
should(currentSelection).not.be.undefined();
|
||||
equalBookItems(currentSelection, expectedNotebook1);
|
||||
});
|
||||
|
||||
@@ -327,8 +328,9 @@ describe('BooksTreeViewTests', function () {
|
||||
});
|
||||
|
||||
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);
|
||||
should(currentSelection).not.be.undefined();
|
||||
equalBookItems(currentSelection, expectedNotebook1);
|
||||
});
|
||||
|
||||
|
||||
@@ -91,9 +91,14 @@ export class SchemaCompareMainWindow {
|
||||
let sourceDacpac = context as string;
|
||||
if (profile) {
|
||||
let ownerUri = await azdata.connection.getUriForConnection((profile.id));
|
||||
let usr = profile.userName;
|
||||
if (!usr) {
|
||||
usr = loc.defaultText;
|
||||
}
|
||||
|
||||
this.sourceEndpointInfo = {
|
||||
endpointType: mssql.SchemaCompareEndpointType.Database,
|
||||
serverDisplayName: `${profile.serverName} ${profile.userName}`,
|
||||
serverDisplayName: `${profile.serverName} (${usr})`,
|
||||
serverName: profile.serverName,
|
||||
databaseName: profile.databaseName,
|
||||
ownerUri: ownerUri,
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
"name": "sql-database-projects",
|
||||
"displayName": "SQL Database Projects",
|
||||
"description": "The SQL Database Projects extension for Azure Data Studio allows users to develop and publish database schemas.",
|
||||
"version": "0.8.0",
|
||||
"version": "0.8.2",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"engines": {
|
||||
"vscode": "^1.30.1",
|
||||
"azdata": ">=1.27.0"
|
||||
"azdata": ">=1.28.0"
|
||||
},
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
"icon": "images/sqlDatabaseProjects.png",
|
||||
|
||||
@@ -52,61 +52,65 @@ export class ProjectsController {
|
||||
this.buildHelper = new BuildHelper();
|
||||
}
|
||||
|
||||
public get dashboardDeployData(): (string | dataworkspace.IconCellValue)[][] {
|
||||
public getDashboardDeployData(projectFile: string): (string | dataworkspace.IconCellValue)[][] {
|
||||
const infoRows: (string | dataworkspace.IconCellValue)[][] = [];
|
||||
let count = 0;
|
||||
|
||||
for (let i = this.deployInfo.length - 1; i >= 0; i--) {
|
||||
let icon: azdata.IconPath;
|
||||
let text: string;
|
||||
if (this.deployInfo[i].status === Status.success) {
|
||||
icon = IconPathHelper.success;
|
||||
text = constants.Success;
|
||||
} else if (this.deployInfo[i].status === Status.failed) {
|
||||
icon = IconPathHelper.error;
|
||||
text = constants.Failed;
|
||||
} else {
|
||||
icon = IconPathHelper.inProgress;
|
||||
text = constants.InProgress;
|
||||
}
|
||||
if (this.deployInfo[i].projectFile === projectFile) {
|
||||
let icon: azdata.IconPath;
|
||||
let text: string;
|
||||
if (this.deployInfo[i].status === Status.success) {
|
||||
icon = IconPathHelper.success;
|
||||
text = constants.Success;
|
||||
} else if (this.deployInfo[i].status === Status.failed) {
|
||||
icon = IconPathHelper.error;
|
||||
text = constants.Failed;
|
||||
} else {
|
||||
icon = IconPathHelper.inProgress;
|
||||
text = constants.InProgress;
|
||||
}
|
||||
|
||||
let infoRow: (string | dataworkspace.IconCellValue)[] = [count.toString(),
|
||||
{ text: text, icon: icon },
|
||||
this.deployInfo[i].target,
|
||||
this.deployInfo[i].timeToCompleteAction,
|
||||
this.deployInfo[i].startDate];
|
||||
infoRows.push(infoRow);
|
||||
count++;
|
||||
let infoRow: (string | dataworkspace.IconCellValue)[] = [count.toString(),
|
||||
{ text: text, icon: icon },
|
||||
this.deployInfo[i].target,
|
||||
this.deployInfo[i].timeToCompleteAction,
|
||||
this.deployInfo[i].startDate];
|
||||
infoRows.push(infoRow);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return infoRows;
|
||||
}
|
||||
|
||||
public get dashboardBuildData(): (string | dataworkspace.IconCellValue)[][] {
|
||||
public getDashboardBuildData(projectFile: string): (string | dataworkspace.IconCellValue)[][] {
|
||||
const infoRows: (string | dataworkspace.IconCellValue)[][] = [];
|
||||
let count = 0;
|
||||
|
||||
for (let i = this.buildInfo.length - 1; i >= 0; i--) {
|
||||
let icon: azdata.IconPath;
|
||||
let text: string;
|
||||
if (this.buildInfo[i].status === Status.success) {
|
||||
icon = IconPathHelper.success;
|
||||
text = constants.Success;
|
||||
} else if (this.buildInfo[i].status === Status.failed) {
|
||||
icon = IconPathHelper.error;
|
||||
text = constants.Failed;
|
||||
} else {
|
||||
icon = IconPathHelper.inProgress;
|
||||
text = constants.InProgress;
|
||||
}
|
||||
if (this.buildInfo[i].projectFile === projectFile) {
|
||||
let icon: azdata.IconPath;
|
||||
let text: string;
|
||||
if (this.buildInfo[i].status === Status.success) {
|
||||
icon = IconPathHelper.success;
|
||||
text = constants.Success;
|
||||
} else if (this.buildInfo[i].status === Status.failed) {
|
||||
icon = IconPathHelper.error;
|
||||
text = constants.Failed;
|
||||
} else {
|
||||
icon = IconPathHelper.inProgress;
|
||||
text = constants.InProgress;
|
||||
}
|
||||
|
||||
let infoRow: (string | dataworkspace.IconCellValue)[] = [count.toString(),
|
||||
{ text: text, icon: icon },
|
||||
this.buildInfo[i].target,
|
||||
this.buildInfo[i].timeToCompleteAction,
|
||||
this.buildInfo[i].startDate];
|
||||
infoRows.push(infoRow);
|
||||
count++;
|
||||
let infoRow: (string | dataworkspace.IconCellValue)[] = [count.toString(),
|
||||
{ text: text, icon: icon },
|
||||
this.buildInfo[i].target,
|
||||
this.buildInfo[i].timeToCompleteAction,
|
||||
this.buildInfo[i].startDate];
|
||||
infoRows.push(infoRow);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return infoRows;
|
||||
@@ -176,7 +180,7 @@ export class ProjectsController {
|
||||
const startTime = new Date();
|
||||
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);
|
||||
|
||||
if (this.buildInfo.length - 1 === maxTableLength) {
|
||||
@@ -276,7 +280,7 @@ export class ProjectsController {
|
||||
const actionStartTime = currentDate.getTime();
|
||||
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);
|
||||
|
||||
if (this.deployInfo.length - 1 === maxTableLength) {
|
||||
@@ -315,7 +319,7 @@ export class ProjectsController {
|
||||
telemetryProps.totalDuration = (actionEndTime - buildStartTime).toString();
|
||||
|
||||
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);
|
||||
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.ProjectController, TelemetryActions.publishProject)
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export class DashboardData {
|
||||
public projectFile: string;
|
||||
public status: Status;
|
||||
public target: string;
|
||||
public timeToCompleteAction: 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.target = target;
|
||||
this.timeToCompleteAction = '';
|
||||
|
||||
@@ -128,7 +128,7 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide
|
||||
/**
|
||||
* Gets the data to be displayed in the project dashboard
|
||||
*/
|
||||
get dashboardComponents(): dataworkspace.IDashboardTable[] {
|
||||
getDashboardComponents(projectFile: string): dataworkspace.IDashboardTable[] {
|
||||
const deployInfo: dataworkspace.IDashboardTable = {
|
||||
name: constants.Deployments,
|
||||
columns: [{ displayName: constants.ID, width: 100 },
|
||||
@@ -136,7 +136,7 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide
|
||||
{ displayName: constants.Target, width: 250 },
|
||||
{ displayName: constants.Time, width: 250 },
|
||||
{ displayName: constants.Date, width: 250 }],
|
||||
data: this.projectController.dashboardDeployData
|
||||
data: this.projectController.getDashboardDeployData(projectFile)
|
||||
};
|
||||
|
||||
const buildInfo: dataworkspace.IDashboardTable = {
|
||||
@@ -146,7 +146,7 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide
|
||||
{ displayName: constants.Target, width: 250 },
|
||||
{ displayName: constants.Time, width: 250 },
|
||||
{ displayName: constants.Date, width: 250 }],
|
||||
data: this.projectController.dashboardBuildData
|
||||
data: this.projectController.getDashboardBuildData(projectFile)
|
||||
};
|
||||
|
||||
return [deployInfo, buildInfo];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "azuredatastudio",
|
||||
"version": "1.28.0",
|
||||
"distro": "4462480cd081b5729600b15921dbb445b46b0de9",
|
||||
"distro": "1d8bd1032738ec5b6aad0b80551eee7376a617dc",
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
},
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
"builtInExtensions": [
|
||||
{
|
||||
"name": "Microsoft.sqlservernotebook",
|
||||
"version": "0.3.9",
|
||||
"version": "0.4.0",
|
||||
"repo": "https://github.com/Microsoft/azuredatastudio"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -37,7 +37,7 @@ export class HeaderFilter<T extends Slick.SlickData> {
|
||||
private columnDef!: FilterableColumn<T>;
|
||||
private buttonStyles?: IButtonStyles;
|
||||
private disposableStore = new DisposableStore();
|
||||
public enabled: boolean = true;
|
||||
private _enabled: boolean = true;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
@@ -435,4 +435,18 @@ export class HeaderFilter<T extends Slick.SlickData> {
|
||||
e.preventDefault();
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class TelemetryEventImpl implements ITelemetryEvent {
|
||||
assign(this._properties,
|
||||
{
|
||||
authenticationType: connectionInfo?.authenticationType,
|
||||
providerName: connectionInfo?.providerName
|
||||
provider: connectionInfo?.providerName
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -75,12 +75,17 @@ export enum TelemetryAction {
|
||||
RunQueryString = 'RunQueryString',
|
||||
ShowChart = 'ShowChart',
|
||||
StopAgentJob = 'StopAgentJob',
|
||||
WizardPagesNavigation = 'WizardPagesNavigation'
|
||||
WizardPagesNavigation = 'WizardPagesNavigation',
|
||||
SearchStarted = 'SearchStarted',
|
||||
SearchCompleted = 'SearchCompleted'
|
||||
}
|
||||
|
||||
export enum NbTelemetryAction {
|
||||
RunCell = 'RunCell',
|
||||
RunAll = 'RunNotebook'
|
||||
RunAll = 'RunNotebook',
|
||||
AddCell = 'AddCell',
|
||||
KernelChanged = 'KernelChanged',
|
||||
NewNotebookFromConnections = 'NewNotebookWithConnectionProfile'
|
||||
}
|
||||
|
||||
export enum TelemetryPropertyName {
|
||||
|
||||
@@ -19,6 +19,9 @@ import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilit
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
|
||||
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> {
|
||||
const backupUiService = accessor.get(IBackupUiService);
|
||||
@@ -43,7 +46,7 @@ export class BackupAction extends Task {
|
||||
const configurationService = accessor.get<IConfigurationService>(IConfigurationService);
|
||||
const previewFeaturesEnabled = configurationService.getValue<{ enablePreviewFeatures: boolean }>('workbench').enablePreviewFeatures;
|
||||
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);
|
||||
@@ -55,17 +58,20 @@ export class BackupAction extends Task {
|
||||
if (profile) {
|
||||
const serverInfo = connectionManagementService.getServerInfo(profile.id);
|
||||
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) {
|
||||
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 instantiationService = accessor.get(IInstantiationService);
|
||||
profile = profile ? profile : new ConnectionProfile(capabilitiesService, profile);
|
||||
if (!profile.databaseName) {
|
||||
return accessor.get<INotificationService>(INotificationService).info(backupNotSupportedOutOfDBContext);
|
||||
}
|
||||
return instantiationService.invokeFunction(showBackup, profile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export class EditorReplacementContribution implements IWorkbenchContribution {
|
||||
if (!language) {
|
||||
// Attempt to use extension or extension of modified input (if in diff editor)
|
||||
// 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) {
|
||||
|
||||
@@ -128,8 +128,10 @@ export default class WebViewComponent extends ComponentBase<WebViewProperties> i
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
if (WebViewComponent.standardSupportedLinkSchemes.indexOf(link.scheme) >= 0 || this.enableCommandUris && link.scheme === 'command') {
|
||||
if (WebViewComponent.standardSupportedLinkSchemes.indexOf(link.scheme) >= 0) {
|
||||
this._openerService.open(link);
|
||||
} else if (this.enableCommandUris && link.scheme === 'command') {
|
||||
this._openerService.open(link, { allowCommands: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ export class LinkCalloutDialog extends Modal {
|
||||
dialogPosition: DialogPosition,
|
||||
dialogProperties: IDialogProperties,
|
||||
private readonly _defaultLabel: string = '',
|
||||
private readonly _defaultLinkUrl: string = '',
|
||||
@IContextViewService private readonly _contextViewService: IContextViewService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@ILayoutService layoutService: ILayoutService,
|
||||
@@ -134,6 +135,7 @@ export class LinkCalloutDialog extends Modal {
|
||||
placeholder: constants.linkAddressPlaceholder,
|
||||
ariaLabel: constants.linkAddressLabel
|
||||
});
|
||||
this._linkUrlInputBox.value = this._defaultLinkUrl;
|
||||
DOM.append(linkAddressRow, linkAddressInputContainer);
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ export class LinkHandlerDirective {
|
||||
this.openerService.open(uri, { openExternal: true }).catch(onUnexpectedError);
|
||||
}
|
||||
else {
|
||||
this.openerService.open(uri).catch(onUnexpectedError);
|
||||
this.openerService.open(uri, { allowCommands: true }).catch(onUnexpectedError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,11 +236,14 @@ export class MarkdownToolbarComponent extends AngularDisposable {
|
||||
needsTransform = false;
|
||||
} else {
|
||||
let linkUrl = linkCalloutResult.insertUnescapedLinkUrl;
|
||||
const isFile = URI.parse(linkUrl).scheme === 'file';
|
||||
if (isFile && !path.isAbsolute(linkUrl)) {
|
||||
const notebookDirName = path.dirname(this.cellModel?.notebookModel?.notebookUri.fsPath);
|
||||
const relativePath = (linkUrl).replace(/\\/g, path.posix.sep);
|
||||
linkUrl = path.resolve(notebookDirName, relativePath);
|
||||
const isAnchorLink = linkUrl.startsWith('#');
|
||||
if (!isAnchorLink) {
|
||||
const isFile = URI.parse(linkUrl).scheme === 'file';
|
||||
if (isFile && !path.isAbsolute(linkUrl)) {
|
||||
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.
|
||||
this.output?.nativeElement?.focus();
|
||||
@@ -296,7 +299,8 @@ export class MarkdownToolbarComponent extends AngularDisposable {
|
||||
|
||||
if (type === MarkdownButtonType.LINK_PREVIEW) {
|
||||
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();
|
||||
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 {
|
||||
// If control doesn't exist, editor may have been destroyed previously when switching edit modes
|
||||
if (!this._cellEditor?.getEditor()?.getControl()) {
|
||||
|
||||
@@ -132,15 +132,28 @@ export class HTMLMarkdownConverter {
|
||||
this.turndownService.addRule('a', {
|
||||
filter: 'a',
|
||||
replacement: (content, node) => {
|
||||
//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)
|
||||
const notebookLink = node.href ? URI.parse(node.href) : URI.file(node.title);
|
||||
const notebookFolder = this.notebookUri ? path.join(path.dirname(this.notebookUri.fsPath), path.sep) : '';
|
||||
let relativePath = findPathRelativeToContent(notebookFolder, notebookLink);
|
||||
if (relativePath) {
|
||||
return `[${node.innerText}](${relativePath})`;
|
||||
let href = node.href;
|
||||
let notebookLink: URI | undefined;
|
||||
const isAnchorLinkInFile = (node.attributes.href?.nodeValue.startsWith('#') || href.includes('#')) && href.startsWith('file://');
|
||||
if (isAnchorLinkInFile) {
|
||||
notebookLink = getUriAnchorLink(node, this.notebookUri);
|
||||
} else {
|
||||
//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
|
||||
@@ -275,7 +288,7 @@ function blankReplacement(content, node) {
|
||||
export function findPathRelativeToContent(notebookFolder: string, contentPath: URI | undefined): string {
|
||||
if (notebookFolder) {
|
||||
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
|
||||
relativePath = relativePath.replace(/\s/g, '%20');
|
||||
if (relativePath.startsWith(path.join('..', path.sep) || path.join('.', path.sep))) {
|
||||
@@ -295,3 +308,15 @@ export function addHighlightIfYellowBgExists(node, content: string): string {
|
||||
}
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ import { INotebookService } from 'sql/workbench/services/notebook/browser/notebo
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { CellContext } from 'sql/workbench/contrib/notebook/browser/cellViews/codeActions';
|
||||
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...");
|
||||
export const msgChanging = localize('changing', "Changing kernel...");
|
||||
@@ -46,7 +48,8 @@ export class AddCellAction extends Action {
|
||||
|
||||
constructor(
|
||||
id: string, label: string, cssClass: string,
|
||||
@INotebookService private _notebookService: INotebookService
|
||||
@INotebookService private _notebookService: INotebookService,
|
||||
@IAdsTelemetryService private _telemetryService: IAdsTelemetryService,
|
||||
) {
|
||||
super(id, label, cssClass);
|
||||
}
|
||||
@@ -68,6 +71,9 @@ export class AddCellAction extends Action {
|
||||
const index = editor.cells?.findIndex(cell => cell.active) ?? 0;
|
||||
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(
|
||||
id: string, label: string, cssClass: string,
|
||||
@INotificationService private notificationService: INotificationService,
|
||||
@INotebookService private _notebookService: INotebookService
|
||||
@INotebookService private _notebookService: INotebookService,
|
||||
@IAdsTelemetryService private _telemetryService: IAdsTelemetryService,
|
||||
) {
|
||||
super(id, label, cssClass);
|
||||
}
|
||||
public async run(context: URI): Promise<boolean> {
|
||||
try {
|
||||
this._telemetryService.sendActionEvent(TelemetryKeys.TelemetryView.Notebook, TelemetryKeys.NbTelemetryAction.RunAll);
|
||||
const editor = this._notebookService.findNotebookEditor(context);
|
||||
await editor.runAllCells();
|
||||
return true;
|
||||
@@ -280,7 +288,8 @@ const kernelDropdownElementId = 'kernel-dropdown';
|
||||
export class KernelsDropdown extends SelectBox {
|
||||
private model: NotebookModel;
|
||||
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);
|
||||
|
||||
if (modelReady) {
|
||||
@@ -549,13 +558,17 @@ export class NewNotebookAction extends Action {
|
||||
id: string,
|
||||
label: string,
|
||||
@ICommandService private commandService: ICommandService,
|
||||
@IObjectExplorerService private objectExplorerService: IObjectExplorerService
|
||||
@IObjectExplorerService private objectExplorerService: IObjectExplorerService,
|
||||
@IAdsTelemetryService private _telemetryService: IAdsTelemetryService,
|
||||
) {
|
||||
super(id, label);
|
||||
this.class = 'notebook-action new-notebook';
|
||||
}
|
||||
|
||||
async run(context?: azdata.ObjectExplorerContext): Promise<void> {
|
||||
this._telemetryService.createActionEvent(TelemetryKeys.TelemetryView.Notebook, TelemetryKeys.NbTelemetryAction.NewNotebookFromConnections)
|
||||
.withConnectionInfo(context?.connectionProfile)
|
||||
.send();
|
||||
let connProfile: azdata.IConnectionProfile;
|
||||
if (context && context.nodeInfo) {
|
||||
let node = await this.objectExplorerService.getTreeNode(context.connectionProfile.id, context.nodeInfo.nodePath);
|
||||
|
||||
@@ -39,6 +39,8 @@ import { NotebookSearchView } from 'sql/workbench/contrib/notebook/browser/noteb
|
||||
import * as path from 'vs/base/common/path';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
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';
|
||||
|
||||
@@ -124,7 +126,8 @@ export class NotebookExplorerViewPaneContainer extends ViewPaneContainer {
|
||||
@IMenuService private menuService: IMenuService,
|
||||
@IContextKeyService private contextKeyService: IContextKeyService,
|
||||
@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);
|
||||
this.inputBoxFocused = Constants.InputBoxFocusedKey.bindTo(this.contextKeyService);
|
||||
@@ -255,6 +258,9 @@ export class NotebookExplorerViewPaneContainer extends ViewPaneContainer {
|
||||
onQueryValidationError(err);
|
||||
return;
|
||||
}
|
||||
this._telemetryService.createActionEvent(TelemetryKeys.TelemetryView.Notebook, TelemetryKeys.TelemetryAction.SearchStarted)
|
||||
.withAdditionalProperties({ triggeredOnType: triggeredOnType })
|
||||
.send();
|
||||
|
||||
this.validateQuery(query).then(() => {
|
||||
if (this.views.length > 1) {
|
||||
|
||||
@@ -43,6 +43,8 @@ import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchStop
|
||||
import { Action, IAction } from 'vs/base/common/actions';
|
||||
import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
||||
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.$;
|
||||
|
||||
@@ -82,6 +84,7 @@ export class NotebookSearchView extends SearchView {
|
||||
@IOpenerService openerService: IOpenerService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@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);
|
||||
@@ -239,6 +242,7 @@ export class NotebookSearchView extends SearchView {
|
||||
}
|
||||
|
||||
public startSearch(query: ITextQuery, excludePatternText: string, includePatternText: string, triggeredOnType: boolean, searchWidget: NotebookSearchWidget): Thenable<void> {
|
||||
let start = new Date().getTime();
|
||||
let progressComplete: () => void;
|
||||
this.progressService.withProgress({ location: this.getProgressLocation(), delay: triggeredOnType ? 300 : 0 }, _progress => {
|
||||
return new Promise<void>(resolve => progressComplete = resolve);
|
||||
@@ -253,6 +257,12 @@ export class NotebookSearchView extends SearchView {
|
||||
}, 2000);
|
||||
|
||||
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);
|
||||
this.state = SearchUIState.Idle;
|
||||
|
||||
|
||||
@@ -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');
|
||||
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');
|
||||
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', () => {
|
||||
|
||||
@@ -24,6 +24,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
|
||||
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
|
||||
import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { NullAdsTelemetryService } from 'sql/platform/telemetry/common/adsTelemetryService';
|
||||
|
||||
class TestClientSession extends ClientSessionStub {
|
||||
private _errorState: boolean = false;
|
||||
@@ -124,7 +125,7 @@ suite('Notebook Actions', function (): void {
|
||||
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;
|
||||
|
||||
// Normal use case
|
||||
@@ -191,7 +192,7 @@ suite('Notebook Actions', function (): void {
|
||||
let mockNotification = TypeMoq.Mock.ofType<INotificationService>(TestNotificationService);
|
||||
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
|
||||
mockNotebookEditor.setup(c => c.runAllCells()).returns(() => Promise.resolve(true));
|
||||
@@ -251,7 +252,7 @@ suite('Notebook Actions', function (): void {
|
||||
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);
|
||||
|
||||
assert.strictEqual(actualCmdId, NewNotebookAction.INTERNAL_NEW_NOTEBOOK_CMD_ID);
|
||||
|
||||
@@ -29,7 +29,7 @@ suite('Link Callout Dialog', function (): 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);
|
||||
linkCalloutDialog.render();
|
||||
|
||||
@@ -50,7 +50,7 @@ suite('Link Callout Dialog', function (): void {
|
||||
test('Should return expected values on insert', async function (): Promise<void> {
|
||||
const defaultLabel = 'defaultLabel';
|
||||
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);
|
||||
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> {
|
||||
const defaultLabel = 'default[]Label';
|
||||
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);
|
||||
linkCalloutDialog.render();
|
||||
|
||||
@@ -106,4 +106,27 @@ suite('Link Callout Dialog', function (): void {
|
||||
assert.equal(escapeUrl('<>&()'), '<>&%28%29', 'URL test known escaped characters failed');
|
||||
assert.equal(escapeUrl('<>&()[]'), '<>&%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');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -430,6 +430,7 @@ export abstract class GridTableBase<T> extends Disposable implements IView {
|
||||
this.renderGridDataRowsRange(startIndex, count);
|
||||
});
|
||||
this.dataProvider.dataRows = collection;
|
||||
this.setFilterState();
|
||||
this.table.updateRowCount();
|
||||
await this.setupState();
|
||||
}
|
||||
@@ -525,7 +526,7 @@ export abstract class GridTableBase<T> extends Disposable implements IView {
|
||||
};
|
||||
this.table.rerenderGrid();
|
||||
}));
|
||||
if (this.configurationService.getValue<boolean>('workbench')['enablePreviewFeatures']) {
|
||||
if (this.enableFilteringFeature) {
|
||||
this.filterPlugin = new HeaderFilter();
|
||||
attachButtonStyler(this.filterPlugin, this.themeService);
|
||||
this.table.registerPlugin(this.filterPlugin);
|
||||
@@ -686,15 +687,25 @@ export abstract class GridTableBase<T> extends Disposable implements IView {
|
||||
public updateResult(resultSet: ResultSetSummary) {
|
||||
this._resultSet = resultSet;
|
||||
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.setFilterState();
|
||||
this.table.updateRowCount();
|
||||
}
|
||||
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 {
|
||||
const selection = this.selectionModel.getSelectedRanges();
|
||||
return <IGridActionContext>{
|
||||
|
||||
@@ -24,6 +24,10 @@ export function showRestore(accessor: ServicesAccessor, connection: IConnectionP
|
||||
}
|
||||
|
||||
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 {
|
||||
public static readonly ID = RestoreFeatureName;
|
||||
@@ -44,7 +48,7 @@ export class RestoreAction extends Task {
|
||||
const configurationService = accessor.get<IConfigurationService>(IConfigurationService);
|
||||
const previewFeaturesEnabled: boolean = configurationService.getValue<{ enablePreviewFeatures: boolean }>('workbench').enablePreviewFeatures;
|
||||
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);
|
||||
@@ -56,13 +60,16 @@ export class RestoreAction extends Task {
|
||||
if (profile) {
|
||||
const serverInfo = connectionManagementService.getServerInfo(profile.id);
|
||||
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 instantiationService = accessor.get(IInstantiationService);
|
||||
profile = profile ? profile : new ConnectionProfile(capabilitiesService, profile);
|
||||
if (!profile.serverName) {
|
||||
return accessor.get<INotificationService>(INotificationService).info(restoreNotSupportedOutOfContext);
|
||||
}
|
||||
return instantiationService.invokeFunction(showRestore, profile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,14 +126,6 @@ export default () => `
|
||||
<p>
|
||||
${escape(localize('welcomePage.documentationBody', "Visit the documentation center for quickstarts, how-to guides, and references for PowerShell, APIs, etc."))}
|
||||
</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">
|
||||
<h2>${escape(localize('welcomePage.videos', "Videos"))}</h2>
|
||||
<div class="flex flex-container-video">
|
||||
|
||||
@@ -20,6 +20,6 @@ export class ModelFactory implements IModelFactory {
|
||||
}
|
||||
|
||||
public createClientSession(options: IClientSessionOptions): IClientSession {
|
||||
return new ClientSession(options);
|
||||
return this.instantiationService.createInstance(ClientSession, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -971,6 +971,12 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
||||
if (kernel.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({
|
||||
newValue: kernel,
|
||||
oldValue: undefined,
|
||||
|
||||
@@ -58,7 +58,7 @@ export class MarkdownRenderer {
|
||||
if (!markdown) {
|
||||
element = document.createElement('span');
|
||||
} else {
|
||||
element = renderMarkdown(markdown, { ...this._getRenderOptions(disposeables), ...options }, markedOptions);
|
||||
element = renderMarkdown(markdown, { ...this._getRenderOptions(markdown, disposeables), ...options }, markedOptions);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -67,7 +67,7 @@ export class MarkdownRenderer {
|
||||
};
|
||||
}
|
||||
|
||||
protected _getRenderOptions(disposeables: DisposableStore): MarkdownRenderOptions {
|
||||
protected _getRenderOptions(markdown: IMarkdownString, disposeables: DisposableStore): MarkdownRenderOptions {
|
||||
return {
|
||||
baseUrl: this._options.baseUrl,
|
||||
codeBlockRenderer: async (languageAlias, value) => {
|
||||
@@ -105,7 +105,7 @@ export class MarkdownRenderer {
|
||||
},
|
||||
asyncRenderCallback: () => this._onDidRenderAsync.fire(),
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,10 +21,15 @@ class CommandOpener implements IOpener {
|
||||
|
||||
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)) {
|
||||
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
|
||||
if (typeof target === 'string') {
|
||||
target = URI.parse(target);
|
||||
|
||||
@@ -808,7 +808,23 @@ export class ViewModel extends Disposable implements IViewModel {
|
||||
|
||||
const fontInfo = this._configuration.options.get(EditorOption.fontInfo);
|
||||
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 {
|
||||
mode: languageId.language,
|
||||
|
||||
@@ -98,14 +98,19 @@ export class CodeLensContribution implements IEditorContribution {
|
||||
const fontFamily = this._editor.getOption(EditorOption.codeLensFontFamily);
|
||||
const editorFontInfo = this._editor.getOption(EditorOption.fontInfo);
|
||||
|
||||
const fontFamilyVar = `--codelens-font-family${this._styleClassName}`;
|
||||
const fontFeaturesVar = `--codelens-font-features${this._styleClassName}`;
|
||||
|
||||
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; }
|
||||
`;
|
||||
if (fontFamily) {
|
||||
newStyle += `.monaco-editor .codelens-decoration.${this._styleClassName} { font-family: ${fontFamily}}`;
|
||||
}
|
||||
this._styleElement.textContent = newStyle;
|
||||
this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily ?? 'inherit');
|
||||
this._editor.getContainerDomNode().style.setProperty(fontFeaturesVar, editorFontInfo.fontFeatureSettings);
|
||||
|
||||
//
|
||||
this._editor.changeViewZones(accessor => {
|
||||
|
||||
@@ -143,7 +143,7 @@ class MessageWidget {
|
||||
this._codeLink.setAttribute('href', `${code.target.toString()}`);
|
||||
|
||||
this._codeLink.onclick = (e) => {
|
||||
this._openerService.open(code.target);
|
||||
this._openerService.open(code.target, { allowCommands: true });
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
@@ -536,7 +536,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget {
|
||||
this._codeLink.setAttribute('href', code.target.toString());
|
||||
|
||||
this._codeLink.onclick = (e) => {
|
||||
this._openerService.open(code.target);
|
||||
this._openerService.open(code.target, { allowCommands: true });
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
@@ -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 => {
|
||||
const messageOrError =
|
||||
|
||||
@@ -81,20 +81,10 @@ suite('OpenerService', function () {
|
||||
const id = `aCommand${Math.random()}`;
|
||||
CommandsRegistry.registerCommand(id, function () { });
|
||||
|
||||
assert.strictEqual(lastCommand, undefined);
|
||||
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.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);
|
||||
assert.strictEqual(lastCommand, undefined);
|
||||
});
|
||||
|
||||
test('links are protected by validators', async function () {
|
||||
@@ -108,6 +98,33 @@ suite('OpenerService', function () {
|
||||
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 () {
|
||||
const openerService = new OpenerService(editorService, commandService);
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ export class Link extends Disposable {
|
||||
|
||||
this._register(onOpen(e => {
|
||||
EventHelper.stop(e, true);
|
||||
openerService.open(link.href);
|
||||
openerService.open(link.href, { allowCommands: true });
|
||||
}));
|
||||
|
||||
this.applyStyles();
|
||||
|
||||
@@ -29,9 +29,18 @@ type OpenInternalOptions = {
|
||||
* action, such as keyboard or mouse usage.
|
||||
*/
|
||||
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;
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface IRemoteAgentEnvironment {
|
||||
workspaceStorageHome: URI;
|
||||
userHome: URI;
|
||||
os: OperatingSystem;
|
||||
useHostProxy: boolean;
|
||||
}
|
||||
|
||||
export interface RemoteAgentConnectionContext {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user