mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-20 18:46:56 -05:00
Compare commits
147 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7553f799e1 | ||
|
|
d64c0549df | ||
|
|
e221c9f421 | ||
|
|
9fdb2161d4 | ||
|
|
b2ca229e60 | ||
|
|
60037222a0 | ||
|
|
106bb5ecdf | ||
|
|
1bcbb93301 | ||
|
|
4fd2d9e76b | ||
|
|
b66031bf16 | ||
|
|
e001cb1da3 | ||
|
|
d70c9f2fa7 | ||
|
|
ebdb1783cc | ||
|
|
e0e6b33610 | ||
|
|
cd1618798e | ||
|
|
ef02a8afae | ||
|
|
8f12e001eb | ||
|
|
e6af8ef531 | ||
|
|
e017675a1c | ||
|
|
6c596d8495 | ||
|
|
2119c96c88 | ||
|
|
4971f1bd1a | ||
|
|
7e99ea8618 | ||
|
|
ff05a4e25d | ||
|
|
e1952b8d12 | ||
|
|
57234a52fd | ||
|
|
c663493690 | ||
|
|
a3acae4777 | ||
|
|
f6d2af58af | ||
|
|
b531958402 | ||
|
|
a7a337f063 | ||
|
|
9ec68087ac | ||
|
|
0e05c32f15 | ||
|
|
4d91a32bed | ||
|
|
f73cf78001 | ||
|
|
5f928a5218 | ||
|
|
a39d73da24 | ||
|
|
b6cf5b2af0 | ||
|
|
315295710d | ||
|
|
db8bd021be | ||
|
|
bb8c02a01b | ||
|
|
315e49b2ed | ||
|
|
c27c5334ba | ||
|
|
f48fa4785d | ||
|
|
2d14665208 | ||
|
|
0550d58579 | ||
|
|
7a6168d9e6 | ||
|
|
973c1c2437 | ||
|
|
57ef5721a3 | ||
|
|
4e8af635c6 | ||
|
|
7564aee5b6 | ||
|
|
ad4a16ca82 | ||
|
|
54d4098f85 | ||
|
|
2b5d2f0a0b | ||
|
|
66115d8f80 | ||
|
|
4ebe4c4547 | ||
|
|
9dee889808 | ||
|
|
3381d59cea | ||
|
|
c56a8b3d8b | ||
|
|
8a43f14b41 | ||
|
|
d139559d98 | ||
|
|
baf52a55ff | ||
|
|
2f746d0b39 | ||
|
|
1cbe11ad38 | ||
|
|
4096037167 | ||
|
|
f72dbccc52 | ||
|
|
453f8e5525 | ||
|
|
4d1be1e288 | ||
|
|
5d23627165 | ||
|
|
8f5b3ef81d | ||
|
|
c37149ccfb | ||
|
|
74c715f16f | ||
|
|
efc7789fe4 | ||
|
|
5e7bc2a05b | ||
|
|
6be1420220 | ||
|
|
6704bc552a | ||
|
|
67ecd6d3d9 | ||
|
|
afe1b4392f | ||
|
|
866fa76a65 | ||
|
|
395bc3f149 | ||
|
|
bf6c503c07 | ||
|
|
ab9e67be30 | ||
|
|
055a0d6c67 | ||
|
|
9fc251259c | ||
|
|
489f5f359f | ||
|
|
28d5382dc3 | ||
|
|
d3073a33fe | ||
|
|
6b1ef0e2ad | ||
|
|
06bb31b944 | ||
|
|
466b80fb21 | ||
|
|
bb4b00a25a | ||
|
|
514b0315cc | ||
|
|
4411a1f319 | ||
|
|
bd98f67113 | ||
|
|
fea7f5156f | ||
|
|
79ba314953 | ||
|
|
518bb33a2f | ||
|
|
441b551c0a | ||
|
|
861215c611 | ||
|
|
59f96ef2e3 | ||
|
|
00bacee1da | ||
|
|
ddefdac6cc | ||
|
|
816cd5a997 | ||
|
|
4ea210e794 | ||
|
|
7da69c82e5 | ||
|
|
78b7c3cfd4 | ||
|
|
db39571394 | ||
|
|
2f1fbe5473 | ||
|
|
33ade09608 | ||
|
|
c2be6447b5 | ||
|
|
7176629e44 | ||
|
|
3ebad4e2b7 | ||
|
|
43e7e35df5 | ||
|
|
7b08ecc4cc | ||
|
|
1367f29a8a | ||
|
|
dc7522c661 | ||
|
|
5334343856 | ||
|
|
1ebf9dcc6a | ||
|
|
3196e99bd6 | ||
|
|
7dd36ae7b4 | ||
|
|
239e7af4e6 | ||
|
|
ada1588bb7 | ||
|
|
2d9720962a | ||
|
|
6d8c66f535 | ||
|
|
96c52ad883 | ||
|
|
6c59779137 | ||
|
|
5e88338423 | ||
|
|
3d2f729586 | ||
|
|
ac80703b75 | ||
|
|
23034cd1bd | ||
|
|
bd36467b99 | ||
|
|
db3dda8519 | ||
|
|
709d15a392 | ||
|
|
d31e33835c | ||
|
|
f321b447f2 | ||
|
|
89dbcb6638 | ||
|
|
102f3794f2 | ||
|
|
697cf93c65 | ||
|
|
18921fc764 | ||
|
|
9e8b80f12f | ||
|
|
bfbb78827f | ||
|
|
d418d7c01b | ||
|
|
77e7a90c20 | ||
|
|
fae9ccb531 | ||
|
|
dbd3a271c4 | ||
|
|
296cf8015a | ||
|
|
97288c421e |
97
CHANGELOG.md
97
CHANGELOG.md
@@ -1,5 +1,81 @@
|
||||
# Change Log
|
||||
|
||||
## Version 1.39.0
|
||||
* Release date: August 24, 2022
|
||||
* Release status: General Availability
|
||||
## What's new in 1.39.0
|
||||
* New Features:
|
||||
* Deployment Wizard - Azure Data Studio now supports SQL Server 2022 (Preview) in the Deployment Wizard for both local and container installation.
|
||||
* Object Explorer - Added Ledger icons and scripting support to Object Explorer for Ledger objects.
|
||||
* Dashboard - Added hexadecimal values to support color detection.
|
||||
* Query Plan Viewer - Added the ability to copy text from cells in the Properties Pane of a query plan.
|
||||
* Query Plan Viewer - Introduced a "find node" option in plan comparison to search for nodes in either the original or added plan.
|
||||
* Table Designer - Now supports the ability to add included columns to a nonclustered index, and the ability to create filtered indexes.
|
||||
* SQL Projects - Publish options were added to the Publish Dialog.
|
||||
* Query History Extension - Added double-click support for query history to either open the query or immediately execute it, based on user configuration.
|
||||
|
||||
* Bug Fixes:
|
||||
* Dashboard - Fixed an accessibility issue that prevented users from being able to access tooltip information using the keyboard.
|
||||
* Voiceover - Fixed a bug that caused voiceover errors across the Dashboard, SQL Projects, SQL Import Wizard, and SQL Migration extensions.
|
||||
* Schema Compare - Fixed a bug that caused the UI to jump back to the top of the options list after selecting/deselecting any option.
|
||||
* Schema Compare - Fixed a bug involving Schema Compare (.SCMP) file incompatibility with Database Project information causing errors when reading and using information stored in this file type.
|
||||
* Object Explorer - Fixed a bug that caused menu items in Object Explorer not to show up for non-English languages.
|
||||
* Table Designer - Fixed a bug that caused the History Table name not to be consistent with the current table name when working with System-Versioned Tables.
|
||||
* Table Designer - Fixed a bug in the Primary Key settings that caused the "Allow Nulls" option to be checked, but disabled, preventing users from changing this option.
|
||||
* Query Editor - Fixed a bug that prevented the SQLCMD in T-SQL from working correctly, giving false errors when running scripts in Azure Data Studio.
|
||||
* Query Editor - Fixed a bug that caused user-specified zoom settings to reset to default when selecting JSON values after query that returned JSON dataset was ran.
|
||||
* SQL Projects - Fixed a bug that caused the "Generate Script" command to not work correctly when targeting a new Azure SQL Database.
|
||||
* Notebooks - Fixed a bug that caused pasted images to disappear from editor after going out of edit mode.
|
||||
* Notebooks - Fixed a bug that caused a console error message to appear after opening a markdown file.
|
||||
* Notebooks - Fixed a bug that prevented markdown cell toolbar shortcuts from working after creating a new split view cell.
|
||||
* Notebooks - Fixed a bug that caused text cells to be erroneously created in split view mode when the notebook default text edit mode was set to "Markdown".
|
||||
|
||||
| Platform |
|
||||
| --------------------------------------- |
|
||||
| [Windows User Installer][win-user] |
|
||||
| [Windows System Installer][win-system] |
|
||||
| [Windows ZIP][win-zip] |
|
||||
| [macOS ZIP][osx-zip] |
|
||||
| [Linux TAR.GZ][linux-zip] |
|
||||
| [Linux RPM][linux-rpm] |
|
||||
| [Linux DEB][linux-deb] |
|
||||
|
||||
[win-user]: https://go.microsoft.com/fwlink/?linkid=2198663
|
||||
[win-system]: https://go.microsoft.com/fwlink/?linkid=2198878
|
||||
[win-zip]: https://go.microsoft.com/fwlink/?linkid=2198664
|
||||
[osx-zip]: https://go.microsoft.com/fwlink/?linkid=2198762
|
||||
[linux-zip]: https://go.microsoft.com/fwlink/?linkid=2198879
|
||||
[linux-rpm]: https://go.microsoft.com/fwlink/?linkid=2198880
|
||||
[linux-deb]: https://go.microsoft.com/fwlink/?linkid=2198763
|
||||
|
||||
## Version 1.38.0
|
||||
* Release date: July 27, 2022
|
||||
* Release status: General Availability
|
||||
## What's new in 1.38.0
|
||||
* New Features:
|
||||
* VS Code merges to 1.62 - This release includes updates to VS Code from the three previous VS Code releases. Read [their release notes](https://code.visualstudio.com/updates/v1_62) to learn more.
|
||||
* Table Designer - New column added to Table Designer for easier access to additional actions specific to individual rows.
|
||||
* Query Plan Viewer - The Top Operations pane view now includes clickable links to operations in each of its rows to show the runtime statistics which can be used to evaluate estimated and actual rows when analyzing a plan.
|
||||
* Query Plan Viewer - Improved UI on selected operation node in the Execution Plan.
|
||||
* Query Plan Viewer - The keyboard command **CTRL + M** no longer executes queries. It now just enables or disables the actual execution plan creation when a query is executed.
|
||||
* Query Plan Viewer - Plan labels are now updated in the Properties window when plans are compared and the orientation is toggled from horizontal to vertical, and back.
|
||||
* Query Plan Viewer - Updates were made to the Command Palette. All execution plan commands are prefixed with "Execution Plan", so that they are easier to find and use.
|
||||
* Query Plan Viewer - A collapse/expand functionality is now available at the operator level to allow users to hide or display sections of the plan during analysis.
|
||||
* Query History - The Query History extension was refactored to be fully implemented in an extension. This makes the history view behave like all other extension views and also allows for searching and filtering in the view by selecting the view and typing in your search text.
|
||||
|
||||
* Bug Fixes:
|
||||
* Table Designer - Error found in edit data tab when switching back to previously selected column when adding a new row. To fix this, editing the table is now disabled while new rows are being added and only reenabled afterwards.
|
||||
* Query Editor - Fixed coloring issues for new T-SQL functions in the Query Editor.
|
||||
* Query Plan Viewer - Fixed bug that caused custom zoom level spinner to allow values outside valid range.
|
||||
* Dashboard - Fixed issue that caused incorrect displaying of insight widgets on the dashboard.
|
||||
* Notebooks - Fixed issue where keyboard shortcuts and toolbar buttons were not working when first creating a Split View markdown cell.
|
||||
* Notebooks - Fixed issue where cell languages were not being set correctly when opening an ADS .NET Interactive notebook in VS Code.
|
||||
* Notebooks - Fixed issue where notebook was being opened as empty when exporting a SQL query as a notebook.
|
||||
* Notebooks - Disables install and uninstall buttons in Manage Packages dialog while a package is being installed or uninstalled.
|
||||
* Notebooks - Fixed issue where cell toolbar buttons were not refreshing when converting cell type.
|
||||
* Notebooks - Fixed issue where notebook was not opening if a cell contains an unsupported output type.
|
||||
* Schema Compare - Fixed issue where views and stored procedures were not correctly recognized by schema compare after applying changes.
|
||||
|
||||
## Version 1.37.0
|
||||
* Release date: June 15, 2022
|
||||
* Release status: General Availability
|
||||
@@ -22,26 +98,7 @@
|
||||
* Schema Compare - Fixed issue with indexes not being added correctly when updating project from database.
|
||||
* Notebooks - Fixed inconsistencies with notebook cell behavior and toolbars.
|
||||
* Notebooks - Fixed issues with keyboard navigation.
|
||||
|
||||
|
||||
| Platform |
|
||||
| --------------------------------------- |
|
||||
| [Windows User Installer][win-user] |
|
||||
| [Windows System Installer][win-system] |
|
||||
| [Windows ZIP][win-zip] |
|
||||
| [macOS ZIP][osx-zip] |
|
||||
| [Linux TAR.GZ][linux-zip] |
|
||||
| [Linux RPM][linux-rpm] |
|
||||
| [Linux DEB][linux-deb] |
|
||||
|
||||
[win-user]: https://go.microsoft.com/fwlink/?linkid=2198663
|
||||
[win-system]: https://go.microsoft.com/fwlink/?linkid=2198878
|
||||
[win-zip]: https://go.microsoft.com/fwlink/?linkid=2198664
|
||||
[osx-zip]: https://go.microsoft.com/fwlink/?linkid=2198762
|
||||
[linux-zip]: https://go.microsoft.com/fwlink/?linkid=2198879
|
||||
[linux-rpm]: https://go.microsoft.com/fwlink/?linkid=2198880
|
||||
[linux-deb]: https://go.microsoft.com/fwlink/?linkid=2198763
|
||||
|
||||
|
||||
## Version 1.36.2
|
||||
* Release date: May 20, 2022
|
||||
* Release status: General Availability
|
||||
|
||||
@@ -121,7 +121,7 @@ steps:
|
||||
set -e
|
||||
DISPLAY=:10 ./scripts/test.sh --build --tfs "Unit Tests" --coverage
|
||||
displayName: Run unit tests (Electron)
|
||||
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'))
|
||||
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'), ne(variables['EXTENSIONS_ONLY'], 'true'))
|
||||
|
||||
- script: |
|
||||
# Figure out the full absolute path of the product we just built
|
||||
@@ -134,7 +134,7 @@ steps:
|
||||
VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/azuredatastudio-reh-linux-x64" \
|
||||
DISPLAY=:10 ./scripts/test-integration.sh --build --tfs "Integration Tests"
|
||||
displayName: Run integration tests (Electron)
|
||||
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'))
|
||||
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'), ne(variables['EXTENSIONS_ONLY'], 'true'))
|
||||
|
||||
- script: |
|
||||
# Figure out the full absolute path of the product we just built
|
||||
@@ -178,11 +178,13 @@ steps:
|
||||
set -e
|
||||
yarn gulp vscode-linux-x64-build-deb
|
||||
displayName: Build Deb
|
||||
condition: and(succeeded(), ne(variables['EXTENSIONS_ONLY'], 'true'))
|
||||
|
||||
- script: |
|
||||
set -e
|
||||
yarn gulp vscode-linux-x64-build-rpm
|
||||
displayName: Build Rpm
|
||||
condition: and(succeeded(), ne(variables['EXTENSIONS_ONLY'], 'true'))
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .NET Core sdk for signing'
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- job: macOS
|
||||
condition: and(succeeded(), eq(variables['VSCODE_BUILD_MACOS'], 'true'), ne(variables['VSCODE_QUALITY'], 'saw'))
|
||||
pool:
|
||||
vmImage: 'macOS-10.15'
|
||||
vmImage: 'macos-latest'
|
||||
dependsOn:
|
||||
- Compile
|
||||
steps:
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- job: macOS_Signing
|
||||
condition: and(succeeded(), eq(variables['VSCODE_BUILD_MACOS'], 'true'), eq(variables['signed'], true), ne(variables['VSCODE_QUALITY'], 'saw'))
|
||||
pool:
|
||||
vmImage: 'macOS-10.15'
|
||||
vmImage: 'macos-latest'
|
||||
dependsOn:
|
||||
- macOS
|
||||
steps:
|
||||
|
||||
@@ -403,7 +403,7 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
||||
.withProps({
|
||||
inputType: 'number',
|
||||
width: '100%',
|
||||
placeHolder: '0'
|
||||
value: '0'
|
||||
})
|
||||
.component();
|
||||
this.retryIntervalBox = view.modelBuilder.inputBox()
|
||||
@@ -411,7 +411,7 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
|
||||
.withProps({
|
||||
inputType: 'number',
|
||||
width: '100%',
|
||||
placeHolder: '0'
|
||||
value: '0'
|
||||
}).component();
|
||||
|
||||
let retryAttemptsContainer = view.modelBuilder.formContainer()
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"is_indirect = arc_data_controller_connectivity_mode == 'Indirect'\n",
|
||||
"is_indirect = arc_data_controller_connectivity_mode.lower() == 'indirect'\n",
|
||||
"\n",
|
||||
"if not is_indirect:\n",
|
||||
"\trun_command('az login')"
|
||||
|
||||
@@ -98,7 +98,8 @@
|
||||
"source": [
|
||||
"print (f'Creating the SQL managed instance - Azure Arc instance')\n",
|
||||
"\n",
|
||||
"is_indirect = arc_data_controller_connection_mode == 'indirect'\n",
|
||||
"is_indirect = arc_data_controller_connectivity_mode.lower() == 'indirect'\n",
|
||||
"is_general_purpose = sql_service_tier == 'GeneralPurpose'\n",
|
||||
"\n",
|
||||
"# Indirect Mode Parameters\n",
|
||||
"retention_days = f' --retention-days \"{sql_retention_days}\"' if is_indirect and sql_retention_days else \"\"\n",
|
||||
@@ -110,8 +111,11 @@
|
||||
"cores_limit_option = f' --cores-limit \"{sql_cores_limit}\"' if sql_cores_limit else \"\"\n",
|
||||
"memory_request_option = f' --memory-request \"{sql_memory_request}Gi\"' if sql_memory_request else \"\"\n",
|
||||
"memory_limit_option = f' --memory-limit \"{sql_memory_limit}Gi\"' if sql_memory_limit else \"\"\n",
|
||||
"readable_secondaries = f' --readable-secondaries \"{sql_readable_secondaries}\"' if sql_readable_secondaries else \"\"\n",
|
||||
"sync_secondary_to_commit = f' --sync-secondary-to-commit \"{sql_sync_secondary_to_commit}\"' if sql_sync_secondary_to_commit else \"\"\n",
|
||||
"\n",
|
||||
"sql_replicas_option = f' --replicas {sql_replicas}' if sql_replicas and not is_general_purpose else \"\"\n",
|
||||
"readable_secondaries = f' --readable-secondaries \"{sql_readable_secondaries}\"' if sql_readable_secondaries and not is_general_purpose else \"\"\n",
|
||||
"sync_secondary_to_commit = f' --sync-secondary-to-commit \"{sql_sync_secondary_to_commit}\"' if sql_sync_secondary_to_commit and not is_general_purpose else \"\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"storage_class_data_option = f' --storage-class-data \"{sql_storage_class_data}\"'if sql_storage_class_data else \"\"\n",
|
||||
"storage_class_datalogs_option = f' --storage-class-datalogs \"{sql_storage_class_datalogs}\"'if sql_storage_class_datalogs else \"\"\n",
|
||||
@@ -130,7 +134,7 @@
|
||||
"\n",
|
||||
"os.environ[\"AZDATA_USERNAME\"] = sql_username\n",
|
||||
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_SQL_PASSWORD\"]\n",
|
||||
"cmd = f'az sql mi-arc create --name {sql_instance_name}{namespace} --replicas {sql_replicas}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}{readable_secondaries}{sync_secondary_to_commit}{storage_class_data_option}{storage_class_datalogs_option}{storage_class_logs_option}{storage_class_backup_option}{volume_size_data}{volume_size_datalogs}{volume_size_logs}{volume_size_backups}{retention_days}{service_tier}{dev_use}{license_type}{cores_limit}{use_k8s}'\n",
|
||||
"cmd = f'az sql mi-arc create --name {sql_instance_name}{namespace}{sql_replicas_option}{cores_request_option}{cores_limit_option}{memory_request_option}{memory_limit_option}{readable_secondaries}{sync_secondary_to_commit}{storage_class_data_option}{storage_class_datalogs_option}{storage_class_logs_option}{storage_class_backup_option}{volume_size_data}{volume_size_datalogs}{volume_size_logs}{volume_size_backups}{retention_days}{service_tier}{dev_use}{license_type}{cores_limit}{use_k8s}'\n",
|
||||
"out=run_command()"
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,8 @@
|
||||
"arc.data.controller.select.cluster.title": "Select from existing Kubernetes clusters",
|
||||
"arc.data.controller.kube.cluster.context": "Cluster context",
|
||||
"arc.data.controller.cluster.config.profile.title": "Choose the config profile",
|
||||
"arc.data.controller.cluster.config.profile": "Config profile",
|
||||
"arc.data.controller.cluster.config.profile": "Kubernetes configuration template",
|
||||
"arc.data.controller.cluster.config.profile.description": "Choose the Kubernetes configuration template that matches with your Kubernetes distribution. This template provides defaults for storage class, service type, etc.",
|
||||
"arc.data.controller.cluster.config.profile.loading": "Loading config profiles",
|
||||
"arc.data.controller.cluster.config.profile.loadingcompleted": "Loading config profiles complete",
|
||||
"arc.data.controller.create.azureconfig.title": "Azure Configuration",
|
||||
@@ -28,7 +29,9 @@
|
||||
"arc.data.controller.project.details.title": "Azure details",
|
||||
"arc.data.controller.project.details.description": "Select the subscription to manage deployed resources and costs. Use resource groups like folders to organize and manage all your resources.",
|
||||
"arc.data.controller.details.title": "Data controller details",
|
||||
"arc.data.controller.details.description": "For indirect mode, provide a namespace, name and storage class for your Azure Arc data controller. This name will be used to identify your Arc instance for remote management and monitoring. For direct mode you do not need to provide a namespace, but please provide the custom location name.",
|
||||
"arc.data.controller.details.description": "For indirect mode, namespace, name and storage class are required. This name will be used to identify your Arc instance for remote management and monitoring. For direct mode, custom location name is required.",
|
||||
"arc.data.controller.indirect.display.name": "Indirect",
|
||||
"arc.data.controller.direct.display.name": "Direct",
|
||||
"arc.data.controller.connectivity.mode": "Connectivity mode",
|
||||
"arc.data.controller.namespace": "Data controller namespace",
|
||||
"arc.data.controller.namespace.description": "Data controller namespace. Indirect mode only.",
|
||||
@@ -36,6 +39,7 @@
|
||||
"arc.data.controller.name": "Data controller name",
|
||||
"arc.data.controller.name.validation.description": "Name must consist of lower case alphanumeric characters, '-' or '.', start/end with an alphanumeric character and be 253 characters or less in length.",
|
||||
"arc.data.controller.location": "Location",
|
||||
"arc.data.controller.location.description": "Location is only used for indirect mode deployment. This field will be ignored for direct mode.",
|
||||
"arc.data.controller.infrastructure": "Infrastructure",
|
||||
"arc.data.controller.custom.location": "Custom Location",
|
||||
"arc.data.controller.custom.location.description": "The name of the custom location. Direct mode only.",
|
||||
@@ -234,6 +238,8 @@
|
||||
"arc.agreement.sql.help.text.learn.more": "Learn more",
|
||||
"arc.agreement.sql.help.text.learn.more.ariaLabel": "Learn more about Azure Arc enabled Managed Instance",
|
||||
"arc.sql.pitr.retention.description": "Configure retention for point-in-time backups. {0}",
|
||||
"arc.data.controller.help.text": "The Kubernetes cluster must already be arc-enabled with the az connectedk8s connect command. Please use our {0} to learn more.",
|
||||
"arc.data.controller.help.text.documentation.page": "documentation page",
|
||||
"arc.agreement.sql.help.text.terms.of.use": "Terms of use",
|
||||
"arc.agreement.sql.help.text.privacy.policy": "Privacy policy",
|
||||
"arc.agreement.sql.help.text.azure.marketplace.terms": "Azure Marketplace Terms"
|
||||
|
||||
@@ -371,3 +371,15 @@ export function getTimeStamp(dateTime: string | undefined): number {
|
||||
export function checkISOTimeString(dateTime: string): boolean {
|
||||
return /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d.*Z/.test(dateTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses out the SQL MIAA list from the raw json output
|
||||
* @param raw The raw version output from az sql mi-arc list
|
||||
*/
|
||||
export function parseMiaaList(raw: string): string | undefined {
|
||||
// The output of az sql mi-arc list looks like:
|
||||
// 'Found 1 Arc-enabled SQL Managed Instances in namespace testns1\r\n[\r\n {\r\n "name": "sqlinstance1",\r\n "primaryEndpoint": "20.236.10.81,1422",\r\n "replicas": "3/3",\r\n "state": "Ready"\r\n }\r\n]'
|
||||
const lines = raw.split('\n');
|
||||
lines.splice(0, 1);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
@@ -253,6 +253,10 @@ export function deletingInstance(name: string): string { return localize('arc.de
|
||||
export function installingExtension(name: string): string { return localize('arc.installingExtension', "Installing extension '{0}'...", name); }
|
||||
export function extensionInstalled(name: string): string { return localize('arc.extensionInstalled', "Extension '{0}' has been installed.", name); }
|
||||
export function updatingInstance(name: string): string { return localize('arc.updatingInstance', "Updating instance '{0}'...", name); }
|
||||
export function upgradingDirectDC(name: string, desiredVersion: string, resourceGroup: string): string { return localize('arc.upgradingDirectDC', "Upgrading data controller '{0}' with command 'az arcdata dc upgrade --desired-version {1} --name {0} --resource-group {2}'", name, desiredVersion, resourceGroup); }
|
||||
export function upgradingIndirectDC(name: string, desiredVersion: string, namespace: string): string { return localize('arc.upgradingIndirectDC', "Upgrading data controller '{0}' with command 'az arcdata dc upgrade --desired-version {1} --name {0} --k8s-namespace {2} --use-k8s'", name, desiredVersion, namespace); }
|
||||
export function upgradingDirectMiaa(name: string, resourceGroup: string): string { return localize('arc.upgradingDirectMiaa', "Upgrading SQL MIAA '{0}' with command 'az sql mi-arc upgrade --name {0} --resource-group {1}'", name, resourceGroup); }
|
||||
export function upgradingIndirectMiaa(name: string, namespace: string): string { return localize('arc.upgradingIndirectMiaa', "Upgrading SQL MIAA '{0}' with command 'az sql mi-arc upgrade --name {0} --k8s-namespace {1} --use-k8s'", name, namespace); }
|
||||
export function instanceDeleted(name: string): string { return localize('arc.instanceDeleted', "Instance '{0}' deleted", name); }
|
||||
export function instanceUpdated(name: string): string { return localize('arc.instanceUpdated', "Instance '{0}' updated", name); }
|
||||
export function extensionsDropped(name: string): string { return localize('arc.extensionsDropped', "Extensions '{0}' dropped", name); }
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { ControllerInfo, ResourceType } from 'arc';
|
||||
import * as azExt from 'az-ext';
|
||||
import * as vscode from 'vscode';
|
||||
import { ConnectionMode } from '../constants';
|
||||
import { parseMiaaList } from '../common/utils';
|
||||
import * as loc from '../localizedConstants';
|
||||
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||
|
||||
@@ -69,15 +69,6 @@ export class ControllerModel {
|
||||
}
|
||||
|
||||
public async refresh(showErrors: boolean = true, namespace: string): Promise<void> {
|
||||
await this.refreshController(showErrors, namespace);
|
||||
if (this._controllerConfig?.spec.settings.azure.connectionMode === ConnectionMode.direct) {
|
||||
await this.refreshDirectMode(this._controllerConfig?.spec.settings.azure.resourceGroup, namespace);
|
||||
} else {
|
||||
await this.refreshIndirectMode(namespace);
|
||||
}
|
||||
}
|
||||
|
||||
public async refreshController(showErrors: boolean = true, namespace: string): Promise<void> {
|
||||
await Promise.all([
|
||||
this._azApi.az.arcdata.dc.config.show(namespace, this.azAdditionalEnvVars).then(result => {
|
||||
this._controllerConfig = result.stdout;
|
||||
@@ -108,38 +99,6 @@ export class ControllerModel {
|
||||
throw err;
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
public async refreshDirectMode(resourceGroup: string, namespace: string): Promise<void> {
|
||||
const newRegistrations: Registration[] = [];
|
||||
await Promise.all([
|
||||
this._azApi.az.postgres.arcserver.list(namespace, this.azAdditionalEnvVars).then(result => {
|
||||
newRegistrations.push(...result.stdout.map(r => {
|
||||
return {
|
||||
instanceName: r.name,
|
||||
state: r.state,
|
||||
instanceType: ResourceType.postgresInstances
|
||||
};
|
||||
}));
|
||||
}),
|
||||
this._azApi.az.sql.miarc.list({ resourceGroup: resourceGroup, namespace: undefined }, this.azAdditionalEnvVars).then(result => {
|
||||
newRegistrations.push(...result.stdout.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);
|
||||
});
|
||||
}
|
||||
|
||||
public async refreshIndirectMode(namespace: string): Promise<void> {
|
||||
const newRegistrations: Registration[] = [];
|
||||
await Promise.all([
|
||||
this._azApi.az.postgres.arcserver.list(namespace, this.azAdditionalEnvVars).then(result => {
|
||||
@@ -152,14 +111,15 @@ export class ControllerModel {
|
||||
}));
|
||||
}),
|
||||
this._azApi.az.sql.miarc.list({ resourceGroup: undefined, namespace: namespace }, this.azAdditionalEnvVars).then(result => {
|
||||
newRegistrations.push(...result.stdout.map(r => {
|
||||
let miaaList = parseMiaaList(result.stdout.toString());
|
||||
let jsonList: azExt.SqlMiListResult[] = JSON.parse(<string>miaaList);
|
||||
newRegistrations.push(...jsonList.map(r => {
|
||||
return {
|
||||
instanceName: r.name,
|
||||
state: r.state,
|
||||
instanceType: ResourceType.sqlManagedInstances
|
||||
};
|
||||
}));
|
||||
|
||||
})
|
||||
]).then(() => {
|
||||
this._registrations = newRegistrations;
|
||||
|
||||
@@ -15,7 +15,6 @@ import { ConnectToMiaaSqlDialog } from '../ui/dialogs/connectMiaaDialog';
|
||||
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||
import { ControllerModel, Registration } from './controllerModel';
|
||||
import { ResourceModel } from './resourceModel';
|
||||
import { ConnectionMode } from '../constants';
|
||||
|
||||
export type DatabaseModel = { name: string, status: string, earliestBackup: string, lastBackup: string };
|
||||
export type RPModel = { recoveryPointObjective: string, retentionDays: string };
|
||||
@@ -100,25 +99,14 @@ export class MiaaModel extends ResourceModel {
|
||||
try {
|
||||
try {
|
||||
let result;
|
||||
if (this.controllerModel.info.connectionMode === ConnectionMode.direct) {
|
||||
result = await this._azApi.az.sql.miarc.show(
|
||||
this.info.name,
|
||||
{
|
||||
resourceGroup: this.controllerModel.info.resourceGroup,
|
||||
namespace: undefined
|
||||
},
|
||||
this.controllerModel.azAdditionalEnvVars
|
||||
);
|
||||
} else {
|
||||
result = await this._azApi.az.sql.miarc.show(
|
||||
this.info.name,
|
||||
{
|
||||
resourceGroup: undefined,
|
||||
namespace: this.controllerModel.info.namespace
|
||||
},
|
||||
this.controllerModel.azAdditionalEnvVars
|
||||
);
|
||||
}
|
||||
result = await this._azApi.az.sql.miarc.show(
|
||||
this.info.name,
|
||||
{
|
||||
resourceGroup: undefined,
|
||||
namespace: this.controllerModel.info.namespace
|
||||
},
|
||||
this.controllerModel.azAdditionalEnvVars
|
||||
);
|
||||
this._config = result.stdout;
|
||||
this.configLastUpdated = new Date();
|
||||
this.rpSettings.retentionDays = this._config?.spec?.backup?.retentionPeriodInDays?.toString() ?? '';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ResourceType } from 'arc';
|
||||
import * as azdata from 'azdata';
|
||||
import * as azurecore from 'azurecore';
|
||||
import * as vscode from 'vscode';
|
||||
import { getConnectionModeDisplayText, getResourceTypeIcon, resourceTypeToDisplayName } from '../../../common/utils';
|
||||
import { getResourceTypeIcon, resourceTypeToDisplayName } from '../../../common/utils';
|
||||
import { cssStyles, IconPathHelper, controllerTroubleshootDocsUrl, iconSize } from '../../../constants';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { ControllerModel } from '../../../models/controllerModel';
|
||||
@@ -147,9 +147,8 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
|
||||
newInstance.onDidClick(async () => {
|
||||
const node = this._controllerModel.treeDataProvider.getControllerNode(this._controllerModel);
|
||||
await vscode.commands.executeCommand('azdata.resource.deploy',
|
||||
'azure-sql-mi', // Default option
|
||||
['azure-sql-mi', 'arc-postgres'], // Type filter
|
||||
{ 'azure-sql-mi': { 'mi-type': ['arc-mi'] } }, // Options filter
|
||||
'arc-sql', // Default option
|
||||
['arc-sql', 'arc-postgres'], // Type filter
|
||||
{ 'CONTROLLER_NAME': node?.label });
|
||||
}));
|
||||
|
||||
@@ -218,7 +217,7 @@ export class ControllerDashboardOverviewPage extends DashboardPage {
|
||||
this.controllerProperties.resourceGroupName = config?.spec.settings.azure.resourceGroup || this.controllerProperties.resourceGroupName;
|
||||
this.controllerProperties.location = this._azurecoreApi.getRegionDisplayName(config?.spec.settings.azure.location) || this.controllerProperties.location;
|
||||
this.controllerProperties.subscriptionId = config?.spec.settings.azure.subscription || this.controllerProperties.subscriptionId;
|
||||
this.controllerProperties.connectionMode = getConnectionModeDisplayText(config?.spec.settings.azure.connectionMode) || this.controllerProperties.connectionMode;
|
||||
this.controllerProperties.connectionMode = config?.spec.settings.azure.connectionMode.toLowerCase() || this.controllerProperties.connectionMode.toLowerCase();
|
||||
this.controllerProperties.instanceNamespace = config?.metadata.namespace || this.controllerProperties.instanceNamespace;
|
||||
this.controllerProperties.status = config?.status.state || this.controllerProperties.status;
|
||||
this.refreshDisplayedProperties();
|
||||
|
||||
@@ -222,15 +222,15 @@ export class ControllerUpgradesPage extends DashboardPage {
|
||||
try {
|
||||
upgradeButton.enabled = false;
|
||||
vscode.window.showInformationMessage(loc.upgradingController('kubectl get datacontrollers -A\' should not be localized.'));
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.updatingInstance(this._controllerModel.info.name),
|
||||
cancellable: true
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
if (nextVersion !== '') {
|
||||
if (this._controllerModel.info.connectionMode === ConnectionMode.direct) {
|
||||
if (this._controllerModel.info.connectionMode.toLowerCase() === ConnectionMode.direct) {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.upgradingDirectDC(this._controllerModel.info.name, nextVersion, this._controllerModel.info.resourceGroup),
|
||||
cancellable: true
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
if (nextVersion !== '') {
|
||||
await this._azApi.az.arcdata.dc.upgrade(
|
||||
nextVersion,
|
||||
this._controllerModel.info.name,
|
||||
@@ -238,24 +238,41 @@ export class ControllerUpgradesPage extends DashboardPage {
|
||||
undefined, // Indirect mode argument - namespace
|
||||
);
|
||||
} else {
|
||||
vscode.window.showInformationMessage(loc.noUpgrades);
|
||||
}
|
||||
try {
|
||||
await this._controllerModel.refresh(false, this._controllerModel.info.namespace);
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.refreshFailed(error));
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.upgradingIndirectDC(this._controllerModel.info.name, nextVersion, this._controllerModel.info.namespace),
|
||||
cancellable: true
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
if (nextVersion !== '') {
|
||||
await this._azApi.az.arcdata.dc.upgrade(
|
||||
nextVersion,
|
||||
this._controllerModel.info.name,
|
||||
undefined, // Direct mode argument - resourceGroup
|
||||
this._controllerModel.info.namespace,
|
||||
);
|
||||
} else {
|
||||
vscode.window.showInformationMessage(loc.noUpgrades);
|
||||
}
|
||||
try {
|
||||
await this._controllerModel.refresh(false, this._controllerModel.info.namespace);
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.refreshFailed(error));
|
||||
}
|
||||
} else {
|
||||
vscode.window.showInformationMessage(loc.noUpgrades);
|
||||
}
|
||||
|
||||
try {
|
||||
await this._controllerModel.refresh(false, this._controllerModel.info.namespace);
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.refreshFailed(error));
|
||||
}
|
||||
}
|
||||
);
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as azExt from 'az-ext';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper, cssStyles, ConnectionMode } from '../../../constants';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { MiaaModel, RPModel, DatabaseModel, systemDbs } from '../../../models/miaaModel';
|
||||
import { ControllerModel } from '../../../models/controllerModel';
|
||||
@@ -220,23 +220,13 @@ export class MiaaBackupsPage extends DashboardPage {
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
if (this._miaaModel.controllerModel.info.connectionMode === ConnectionMode.direct) {
|
||||
await this._azApi.az.sql.miarc.update(
|
||||
this._miaaModel.info.name,
|
||||
this._saveArgs,
|
||||
this._miaaModel.controllerModel.info.resourceGroup,
|
||||
undefined, // Indirect mode argument - namespace
|
||||
undefined, // Indirect mode argument - usek8s
|
||||
this._miaaModel.controllerModel.azAdditionalEnvVars);
|
||||
} else {
|
||||
await this._azApi.az.sql.miarc.update(
|
||||
this._miaaModel.info.name,
|
||||
this._saveArgs,
|
||||
undefined, // Direct mode argument - resourceGroup
|
||||
this._miaaModel.controllerModel.info.namespace,
|
||||
true,
|
||||
this._miaaModel.controllerModel.azAdditionalEnvVars);
|
||||
}
|
||||
await this._azApi.az.sql.miarc.update(
|
||||
this._miaaModel.info.name,
|
||||
this._saveArgs,
|
||||
undefined, // Direct mode argument - resourceGroup
|
||||
this._miaaModel.controllerModel.info.namespace,
|
||||
true,
|
||||
this._miaaModel.controllerModel.azAdditionalEnvVars);
|
||||
try {
|
||||
await this._miaaModel.refresh();
|
||||
} catch (error) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as azExt from 'az-ext';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper, cssStyles, ConnectionMode } from '../../../constants';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { convertToGibibyteString } from '../../../common/utils';
|
||||
import { MiaaModel } from '../../../models/miaaModel';
|
||||
@@ -132,23 +132,13 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
try {
|
||||
if (this._miaaModel.controllerModel.info.connectionMode === ConnectionMode.direct) {
|
||||
await this._azApi.az.sql.miarc.update(
|
||||
this._miaaModel.info.name,
|
||||
this.saveArgs,
|
||||
this._miaaModel.controllerModel.info.resourceGroup,
|
||||
undefined, // Indirect mode argument - namespace
|
||||
undefined, // Indirect mode argument - usek8s
|
||||
this._miaaModel.controllerModel.azAdditionalEnvVars);
|
||||
} else {
|
||||
await this._azApi.az.sql.miarc.update(
|
||||
this._miaaModel.info.name,
|
||||
this.saveArgs,
|
||||
undefined, // Direct mode argument - resourceGroup
|
||||
this._miaaModel.controllerModel.info.namespace,
|
||||
true,
|
||||
this._miaaModel.controllerModel.azAdditionalEnvVars);
|
||||
}
|
||||
await this._azApi.az.sql.miarc.update(
|
||||
this._miaaModel.info.name,
|
||||
this.saveArgs,
|
||||
undefined, // Direct mode argument - resourceGroup
|
||||
this._miaaModel.controllerModel.info.namespace,
|
||||
true,
|
||||
this._miaaModel.controllerModel.azAdditionalEnvVars);
|
||||
} catch (err) {
|
||||
this.saveButton!.enabled = true;
|
||||
throw err;
|
||||
@@ -272,6 +262,7 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
|
||||
this.syncSecondaryToCommitBox = this.modelView.modelBuilder.inputBox().withProps({
|
||||
readOnly: false,
|
||||
min: -1,
|
||||
max: 2,
|
||||
inputType: 'number',
|
||||
placeHolder: loc.loading,
|
||||
ariaLabel: loc.syncSecondaryToCommit
|
||||
@@ -359,8 +350,9 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
|
||||
currentCPUSize = '';
|
||||
}
|
||||
|
||||
this.coresRequestBox!.placeHolder = currentCPUSize;
|
||||
this.coresRequestBox!.value = '';
|
||||
this.coresRequestBox!.value = currentCPUSize;
|
||||
this.coresRequestBox!.placeHolder = '';
|
||||
|
||||
this.saveArgs.coresRequest = undefined;
|
||||
|
||||
currentCPUSize = this._miaaModel.config?.spec?.scheduling?.default?.resources?.limits?.cpu;
|
||||
@@ -371,6 +363,7 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
|
||||
|
||||
this.coresLimitBox!.placeHolder = currentCPUSize;
|
||||
this.coresLimitBox!.value = '';
|
||||
|
||||
this.saveArgs.coresLimit = undefined;
|
||||
}
|
||||
|
||||
@@ -400,6 +393,7 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
|
||||
this.memoryLimitBox!.placeHolder = currentMemSizeConversion!;
|
||||
this.memoryLimitBox!.value = '';
|
||||
|
||||
|
||||
this.saveArgs.memoryLimit = undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as azExt from 'az-ext';
|
||||
import * as azurecore from 'azurecore';
|
||||
import * as vscode from 'vscode';
|
||||
import { getDatabaseStateDisplayText, promptForInstanceDeletion } from '../../../common/utils';
|
||||
import { ConnectionMode, cssStyles, IconPathHelper, miaaTroubleshootDocsUrl } from '../../../constants';
|
||||
import { cssStyles, IconPathHelper, miaaTroubleshootDocsUrl } from '../../../constants';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { ControllerModel } from '../../../models/controllerModel';
|
||||
import { MiaaModel } from '../../../models/miaaModel';
|
||||
@@ -243,25 +243,14 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token) => {
|
||||
if (this._controllerModel.info.connectionMode === ConnectionMode.direct) {
|
||||
return await this._azApi.az.sql.miarc.delete(
|
||||
this._miaaModel.info.name,
|
||||
{
|
||||
resourceGroup: this._controllerModel.info.resourceGroup,
|
||||
namespace: undefined,
|
||||
},
|
||||
this._controllerModel.azAdditionalEnvVars
|
||||
);
|
||||
} else {
|
||||
return await this._azApi.az.sql.miarc.delete(
|
||||
this._miaaModel.info.name,
|
||||
{
|
||||
resourceGroup: undefined,
|
||||
namespace: this._controllerModel.info.namespace,
|
||||
},
|
||||
this._controllerModel.azAdditionalEnvVars
|
||||
);
|
||||
}
|
||||
return await this._azApi.az.sql.miarc.delete(
|
||||
this._miaaModel.info.name,
|
||||
{
|
||||
resourceGroup: undefined,
|
||||
namespace: this._controllerModel.info.namespace,
|
||||
},
|
||||
this._controllerModel.azAdditionalEnvVars
|
||||
);
|
||||
}
|
||||
);
|
||||
await this._controllerModel.refreshTreeNode();
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as azExt from 'az-ext';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper, cssStyles, ConnectionMode } from '../../../constants';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { ControllerModel } from '../../../models/controllerModel';
|
||||
import { UpgradeSqlMiaa } from '../../dialogs/upgradeSqlMiaa';
|
||||
@@ -160,25 +160,14 @@ export class MiaaUpgradeManagementPage extends DashboardPage {
|
||||
private async getMiaaVersion(): Promise<string | undefined> {
|
||||
try {
|
||||
let miaaShowResult;
|
||||
if (this._controllerModel.info.connectionMode === ConnectionMode.direct || this._controllerModel.controllerConfig?.spec.settings.azure.connectionMode === ConnectionMode.direct) {
|
||||
miaaShowResult = await this._azApi.az.sql.miarc.show(
|
||||
this._miaaModel.info.name,
|
||||
{
|
||||
resourceGroup: this._controllerModel.info.resourceGroup,
|
||||
namespace: undefined
|
||||
},
|
||||
this._controllerModel.azAdditionalEnvVars
|
||||
);
|
||||
} else {
|
||||
miaaShowResult = await this._azApi.az.sql.miarc.show(
|
||||
this._miaaModel.info.name,
|
||||
{
|
||||
resourceGroup: undefined,
|
||||
namespace: this._controllerModel.info.namespace
|
||||
},
|
||||
this._controllerModel.azAdditionalEnvVars
|
||||
);
|
||||
}
|
||||
miaaShowResult = await this._azApi.az.sql.miarc.show(
|
||||
this._miaaModel.info.name,
|
||||
{
|
||||
resourceGroup: undefined,
|
||||
namespace: this._controllerModel.info.namespace
|
||||
},
|
||||
this._controllerModel.azAdditionalEnvVars
|
||||
);
|
||||
return miaaShowResult.stdout.status.runningVersion;
|
||||
} catch (e) {
|
||||
console.error(loc.showMiaaError, e);
|
||||
@@ -266,28 +255,17 @@ export class MiaaUpgradeManagementPage extends DashboardPage {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.updatingInstance(this._miaaModel.info.name),
|
||||
title: loc.upgradingIndirectMiaa(this._miaaModel.info.name, this._controllerModel.info.namespace),
|
||||
cancellable: true
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
if (this._controllerModel.info.connectionMode === ConnectionMode.direct) {
|
||||
await this._azApi.az.sql.miarc.upgrade(
|
||||
this._miaaModel.info.name,
|
||||
{
|
||||
resourceGroup: this._controllerModel.info.resourceGroup,
|
||||
namespace: undefined
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await this._azApi.az.sql.miarc.upgrade(
|
||||
this._miaaModel.info.name,
|
||||
{
|
||||
resourceGroup: undefined,
|
||||
namespace: this._controllerModel.info.namespace,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await this._azApi.az.sql.miarc.upgrade(
|
||||
this._miaaModel.info.name,
|
||||
{
|
||||
resourceGroup: undefined,
|
||||
namespace: this._controllerModel.info.namespace,
|
||||
}
|
||||
);
|
||||
try {
|
||||
await this._controllerModel.refresh(false, this._controllerModel.info.namespace);
|
||||
} catch (error) {
|
||||
|
||||
@@ -201,10 +201,10 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
|
||||
// default info.name to the name of the controller instance if the user did not specify their own and to a pre-canned default if for some weird reason controller endpoint returned instanceName is also not a valid value
|
||||
controllerModel.info.name = controllerModel.info.name || controllerModel.controllerConfig?.metadata.name || loc.defaultControllerName;
|
||||
controllerModel.info.resourceGroup = <string>controllerModel.controllerConfig?.spec.settings.azure.resourceGroup;
|
||||
controllerModel.info.connectionMode = <string>controllerModel.controllerConfig?.spec.settings.azure.connectionMode;
|
||||
controllerModel.info.connectionMode = <string>controllerModel.controllerConfig?.spec.settings.azure.connectionMode.toLowerCase();
|
||||
controllerModel.info.location = <string>controllerModel.controllerConfig?.spec.settings.azure.location;
|
||||
|
||||
if (controllerModel.info.connectionMode.toLowerCase() === ConnectionMode.direct.toLowerCase()) {
|
||||
if (controllerModel.info.connectionMode.toLowerCase() === ConnectionMode.direct) {
|
||||
const rawCustomLocation = <string>controllerModel.controllerConfig?.metadata.annotations['management.azure.com/customLocation'];
|
||||
const exp = /custom[lL]ocations\/([\S]*)/;
|
||||
controllerModel.info.customLocation = <string>exp.exec(rawCustomLocation)?.pop();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "azcli",
|
||||
"displayName": "%azcli.arc.displayName%",
|
||||
"description": "%azcli.arc.description%",
|
||||
"version": "1.4.1",
|
||||
"version": "1.5.1",
|
||||
"publisher": "Microsoft",
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
"icon": "images/extension.png",
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
|
||||
import * as azExt from 'az-ext';
|
||||
import { IAzTool } from './az';
|
||||
import Logger from './common/logger';
|
||||
import { NoAzureCLIError } from './common/utils';
|
||||
import * as loc from './localizedConstants';
|
||||
import { AzToolService } from './services/azToolService';
|
||||
|
||||
/**
|
||||
@@ -21,7 +19,6 @@ export function validateAz(az: IAzTool | undefined) {
|
||||
|
||||
export function throwIfNoAz(localAz: IAzTool | undefined): asserts localAz {
|
||||
if (!localAz) {
|
||||
Logger.log(loc.noAzureCLI);
|
||||
throw new NoAzureCLIError();
|
||||
}
|
||||
}
|
||||
@@ -120,9 +117,9 @@ export function getAzApi(localAzDiscovered: Promise<IAzTool | undefined>, azTool
|
||||
delete: async (
|
||||
name: string,
|
||||
args: {
|
||||
// Direct mode arguments
|
||||
// ARM API arguments
|
||||
resourceGroup?: string;
|
||||
// Indirect mode arguments
|
||||
// K8s API arguments
|
||||
namespace?: string;
|
||||
},
|
||||
additionalEnvVars?: azExt.AdditionalEnvVars
|
||||
@@ -133,9 +130,9 @@ export function getAzApi(localAzDiscovered: Promise<IAzTool | undefined>, azTool
|
||||
},
|
||||
list: async (
|
||||
args: {
|
||||
// Direct mode arguments
|
||||
// ARM API arguments
|
||||
resourceGroup?: string;
|
||||
// Indirect mode arguments
|
||||
// K8s API arguments
|
||||
namespace?: string;
|
||||
},
|
||||
additionalEnvVars?: azExt.AdditionalEnvVars
|
||||
@@ -147,9 +144,9 @@ export function getAzApi(localAzDiscovered: Promise<IAzTool | undefined>, azTool
|
||||
show: async (
|
||||
name: string,
|
||||
args: {
|
||||
// Direct mode arguments
|
||||
// ARM API arguments
|
||||
resourceGroup?: string;
|
||||
// Indirect mode arguments
|
||||
// K8s API arguments
|
||||
namespace?: string;
|
||||
},
|
||||
// Additional arguments
|
||||
@@ -169,9 +166,9 @@ export function getAzApi(localAzDiscovered: Promise<IAzTool | undefined>, azTool
|
||||
noWait?: boolean;
|
||||
syncSecondaryToCommit?: string;
|
||||
},
|
||||
// Direct mode arguments
|
||||
// ARM API arguments
|
||||
resourceGroup?: string,
|
||||
// Indirect mode arguments
|
||||
// K8s API arguments
|
||||
namespace?: string,
|
||||
usek8s?: boolean,
|
||||
// Additional arguments
|
||||
@@ -184,9 +181,9 @@ export function getAzApi(localAzDiscovered: Promise<IAzTool | undefined>, azTool
|
||||
upgrade: async (
|
||||
name: string,
|
||||
args: {
|
||||
// Direct mode arguments
|
||||
// ARM API arguments
|
||||
resourceGroup?: string;
|
||||
// Indirect mode arguments
|
||||
// K8s API arguments
|
||||
namespace?: string;
|
||||
},
|
||||
// Additional arguments
|
||||
|
||||
@@ -12,7 +12,7 @@ import * as vscode from 'vscode';
|
||||
import { executeCommand, executeSudoCommand, ExitCodeError, ProcessOutput } from './common/childProcess';
|
||||
import { HttpClient } from './common/httpClient';
|
||||
import Logger from './common/logger';
|
||||
import { AzureCLIArcExtError, NoAzureCLIError, searchForCmd } from './common/utils';
|
||||
import { NoAzureCLIArcExtError, NoAzureCLIError, searchForCmd } from './common/utils';
|
||||
import { azArcdataInstallKey, azConfigSection, azFound, debugConfigKey, latestAzArcExtensionVersion, azCliInstallKey, azArcFound, azHostname, azUri } from './constants';
|
||||
import * as loc from './localizedConstants';
|
||||
|
||||
@@ -116,7 +116,7 @@ export class AzTool implements azExt.IAzApi {
|
||||
const argsArray = ['arcdata', 'dc', 'upgrade', '--desired-version', desiredVersion, '--name', name];
|
||||
// Direct mode argument
|
||||
if (resourceGroup) { argsArray.push('--resource-group', resourceGroup); }
|
||||
// Indirect mode arguments
|
||||
// K8s API arguments
|
||||
if (namespace) {
|
||||
argsArray.push('--k8s-namespace', namespace);
|
||||
argsArray.push('--use-k8s');
|
||||
@@ -180,9 +180,9 @@ export class AzTool implements azExt.IAzApi {
|
||||
delete: (
|
||||
name: string,
|
||||
args: {
|
||||
// Direct mode arguments
|
||||
// ARM API arguments
|
||||
resourceGroup?: string,
|
||||
// Indirect mode arguments
|
||||
// K8s API arguments
|
||||
namespace?: string
|
||||
// Additional arguments
|
||||
},
|
||||
@@ -200,14 +200,14 @@ export class AzTool implements azExt.IAzApi {
|
||||
},
|
||||
list: (
|
||||
args: {
|
||||
// Direct mode arguments
|
||||
// ARM API arguments
|
||||
resourceGroup?: string,
|
||||
// Indirect mode arguments
|
||||
// K8s API arguments
|
||||
namespace?: string
|
||||
// Additional arguments
|
||||
},
|
||||
additionalEnvVars?: azExt.AdditionalEnvVars
|
||||
): Promise<azExt.AzOutput<azExt.SqlMiListResult[]>> => {
|
||||
): Promise<azExt.AzOutput<azExt.SqlMiListRawOutput>> => {
|
||||
const argsArray = ['sql', 'mi-arc', 'list'];
|
||||
if (args.resourceGroup) {
|
||||
argsArray.push('--resource-group', args.resourceGroup);
|
||||
@@ -216,14 +216,15 @@ export class AzTool implements azExt.IAzApi {
|
||||
argsArray.push('--k8s-namespace', args.namespace);
|
||||
argsArray.push('--use-k8s');
|
||||
}
|
||||
return this.executeCommand<azExt.SqlMiListResult[]>(argsArray, additionalEnvVars);
|
||||
return this.executeCommand<azExt.SqlMiListRawOutput>(argsArray, additionalEnvVars);
|
||||
|
||||
},
|
||||
show: (
|
||||
name: string,
|
||||
args: {
|
||||
// Direct mode arguments
|
||||
// ARM API arguments
|
||||
resourceGroup?: string,
|
||||
// Indirect mode arguments
|
||||
// K8s API arguments
|
||||
namespace?: string
|
||||
// Additional arguments
|
||||
},
|
||||
@@ -250,9 +251,9 @@ export class AzTool implements azExt.IAzApi {
|
||||
retentionDays?: string,
|
||||
syncSecondaryToCommit?: string
|
||||
},
|
||||
// Direct mode arguments
|
||||
// ARM API arguments
|
||||
resourceGroup?: string,
|
||||
// Indirect mode arguments
|
||||
// K8s API arguments
|
||||
namespace?: string,
|
||||
usek8s?: boolean,
|
||||
// Additional arguments
|
||||
@@ -265,6 +266,7 @@ export class AzTool implements azExt.IAzApi {
|
||||
if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
|
||||
if (args.noWait) { argsArray.push('--no-wait'); }
|
||||
if (args.retentionDays) { argsArray.push('--retention-days', args.retentionDays); }
|
||||
if (args.syncSecondaryToCommit) { argsArray.push('--sync-secondary-to-commit', args.syncSecondaryToCommit); }
|
||||
if (resourceGroup) { argsArray.push('--resource-group', resourceGroup); }
|
||||
if (namespace) { argsArray.push('--k8s-namespace', namespace); }
|
||||
if (usek8s) { argsArray.push('--use-k8s'); }
|
||||
@@ -273,9 +275,9 @@ export class AzTool implements azExt.IAzApi {
|
||||
upgrade: (
|
||||
name: string,
|
||||
args: {
|
||||
// Direct mode arguments
|
||||
// ARM API arguments
|
||||
resourceGroup?: string,
|
||||
// Indirect mode arguments
|
||||
// K8s API arguments
|
||||
namespace?: string
|
||||
// Additional arguments
|
||||
},
|
||||
@@ -447,7 +449,7 @@ export async function checkAndInstallAz(userRequested: boolean = false): Promise
|
||||
try {
|
||||
return await findAzAndArc(); // find currently installed Az
|
||||
} catch (err) {
|
||||
if (err === AzureCLIArcExtError) {
|
||||
if (err instanceof NoAzureCLIArcExtError) {
|
||||
// Az found but arcdata extension not found. Prompt user to install it, then check again.
|
||||
if (await promptToInstallArcdata(userRequested)) {
|
||||
return await findAzAndArc();
|
||||
@@ -478,7 +480,7 @@ export async function findAzAndArc(): Promise<IAzTool> {
|
||||
Logger.log(loc.foundExistingAz(await azTool.getPath(), (await azTool.getSemVersionAz()).raw, (await azTool.getSemVersionArc()).raw));
|
||||
return azTool;
|
||||
} catch (err) {
|
||||
if (err === AzureCLIArcExtError) {
|
||||
if (err === NoAzureCLIArcExtError) {
|
||||
Logger.log(loc.couldNotFindAzArc(err));
|
||||
Logger.log(loc.noAzArc);
|
||||
await vscode.commands.executeCommand('setContext', azArcFound, false); // save a context key that az was not found so that command for installing az is available in commandPalette and that for updating it is no longer available.
|
||||
@@ -506,7 +508,7 @@ async function findSpecificAzAndArc(): Promise<IAzTool> {
|
||||
// if no az has been found. If found, check if az arcdata extension exists.
|
||||
const arcVersion = parseArcExtensionVersion(versionOutput.stdout);
|
||||
if (arcVersion === undefined) {
|
||||
throw AzureCLIArcExtError;
|
||||
throw new NoAzureCLIArcExtError;
|
||||
}
|
||||
|
||||
// Quietly attempt to update the arcdata extension to the latest. If it is already the latest, then it will not update.
|
||||
|
||||
@@ -17,7 +17,7 @@ export class NoAzureCLIError extends Error implements azExt.ErrorWithLink {
|
||||
}
|
||||
}
|
||||
|
||||
export class AzureCLIArcExtError extends Error implements azExt.ErrorWithLink {
|
||||
export class NoAzureCLIArcExtError extends Error implements azExt.ErrorWithLink {
|
||||
constructor() {
|
||||
super(loc.arcdataExtensionNotInstalled);
|
||||
}
|
||||
|
||||
27
extensions/azcli/src/typings/az-ext.d.ts
vendored
27
extensions/azcli/src/typings/az-ext.d.ts
vendored
@@ -29,6 +29,11 @@ declare module 'az-ext' {
|
||||
protocol: string // "https"
|
||||
}
|
||||
|
||||
export interface SqlMiListRawOutput {
|
||||
text: string,
|
||||
miaaList: SqlMiListResult[]
|
||||
}
|
||||
|
||||
export interface SqlMiListResult {
|
||||
name: string, // "arc-miaa"
|
||||
replicas: string, // "1/1"
|
||||
@@ -575,9 +580,9 @@ declare module 'az-ext' {
|
||||
delete(
|
||||
name: string,
|
||||
args: {
|
||||
// Direct mode arguments
|
||||
// ARM API arguments
|
||||
resourceGroup?: string,
|
||||
// Indirect mode arguments
|
||||
// K8s API arguments
|
||||
namespace?: string
|
||||
},
|
||||
// Additional arguments
|
||||
@@ -585,20 +590,20 @@ declare module 'az-ext' {
|
||||
): Promise<AzOutput<void>>,
|
||||
list(
|
||||
args: {
|
||||
// Direct mode arguments
|
||||
// ARM API arguments
|
||||
resourceGroup?: string,
|
||||
// Indirect mode arguments
|
||||
// K8s API arguments
|
||||
namespace?: string
|
||||
},
|
||||
// Additional arguments
|
||||
additionalEnvVars?: AdditionalEnvVars
|
||||
): Promise<AzOutput<SqlMiListResult[]>>,
|
||||
): Promise<AzOutput<SqlMiListRawOutput>>,
|
||||
show(
|
||||
name: string,
|
||||
args: {
|
||||
// Direct mode arguments
|
||||
// ARM API arguments
|
||||
resourceGroup?: string,
|
||||
// Indirect mode arguments
|
||||
// K8s API arguments
|
||||
namespace?: string
|
||||
},
|
||||
// Additional arguments
|
||||
@@ -615,9 +620,9 @@ declare module 'az-ext' {
|
||||
retentionDays?: string, //5
|
||||
syncSecondaryToCommit?: string //2
|
||||
},
|
||||
// Direct mode arguments
|
||||
// ARM API arguments
|
||||
resourceGroup?: string,
|
||||
// Indirect mode arguments
|
||||
// K8s API arguments
|
||||
namespace?: string,
|
||||
usek8s?: boolean,
|
||||
// Additional arguments
|
||||
@@ -626,9 +631,9 @@ declare module 'az-ext' {
|
||||
upgrade(
|
||||
name: string,
|
||||
args: {
|
||||
// Direct mode arguments
|
||||
// ARM API arguments
|
||||
resourceGroup?: string,
|
||||
// Indirect mode arguments
|
||||
// K8s API arguments
|
||||
namespace?: string
|
||||
},
|
||||
// Additional arguments
|
||||
|
||||
@@ -42,12 +42,11 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeN
|
||||
}
|
||||
|
||||
if (!this.initialized) {
|
||||
this.loadSavedControllers().catch(err => { vscode.window.showErrorMessage(localize('bdc.controllerTreeDataProvider.error', "Unexpected error loading saved controllers: {0}", err)); });
|
||||
} else {
|
||||
// We set the context here since VS Code takes a bit of time to process the _onDidChangeTreeData
|
||||
// and so if we set it as soon as we finished loading the controllers it would briefly flash
|
||||
// the "connect to controller" welcome view
|
||||
await vscode.commands.executeCommand('setContext', 'bdc.loaded', true);
|
||||
try {
|
||||
await this.loadSavedControllers();
|
||||
} catch (err) {
|
||||
void vscode.window.showErrorMessage(localize('bdc.controllerTreeDataProvider.error', "Unexpected error loading saved controllers: {0}", err));
|
||||
}
|
||||
}
|
||||
|
||||
return this.root.getChildren();
|
||||
@@ -132,13 +131,12 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeN
|
||||
|
||||
this.root.clearChildren();
|
||||
treeNodes.forEach(node => this.root.addChild(node));
|
||||
this.notifyNodeChanged();
|
||||
await vscode.commands.executeCommand('setContext', 'bdc.loaded', true);
|
||||
} catch (err) {
|
||||
// Reset so we can try again if the tree refreshes
|
||||
this.initialized = false;
|
||||
throw err;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async saveControllers(): Promise<void> {
|
||||
|
||||
@@ -68,6 +68,10 @@ export class DacFxTestService implements mssql.IDacFxService {
|
||||
booleanOptionsDictionary: {
|
||||
'SampleProperty1': { value: false, description: sampleDesc, displayName: sampleName },
|
||||
'SampleProperty2': { value: false, description: sampleDesc, displayName: sampleName }
|
||||
},
|
||||
objectTypesDictionary: {
|
||||
'ObjectType1': sampleName,
|
||||
'ObjectType2': sampleName
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
## Integration tests
|
||||
The integration-tests suite is based on the extension testing feature provided by VS Code, We can use this for:
|
||||
* Commands for setting up the environment for feature testing.
|
||||
* Adding test cases that do not need UI interaction or the test scenarios not supported by the UI automation framework (e.g. object explorer context menu – not html based)
|
||||
|
||||
This extension is for running tests against specific features that require a connection to an actual server.
|
||||
|
||||
Unit tests that don't require this should be added as tests to the extensions or core directly.
|
||||
|
||||
Tests that require user interaction should be added to the smoke tests - see https://github.com/microsoft/azuredatastudio/blob/main/test/smoke/README.md for more information.
|
||||
|
||||
##### Folders
|
||||
* extensionInstallers folder: Copy the VISX installers for the extensions we would like to run the tests with.
|
||||
* src folder: This is where the test file for features should be added, name the file like this: feature.test.ts. e.g. objectExplorer.test.ts
|
||||
|
||||
## UI automation testing
|
||||
The UI automation test cases should be added under $root/test/smoke/src/sql folder. Each feature should create its own folder and add 2 files, one for accessing the feature and the other for the test cases. For example: objectExplorer.ts and objectExplorer.test.ts. only tested on Windows for now.
|
||||
|
||||
For both Smoke test and Integration test, ADS will be launched using new temp folders: extension folder and data folder so that your local dev environment won't be changed.
|
||||
* `extensionInstallers` folder: VSIX packages of non-builtin extensions should be put here for the tests to run with, they will be installed upon startup of the tests.
|
||||
* `src/test` folder: This is where the test files for features should be added, name the file like this: `feature.test.ts` e.g. `objectExplorer.test.ts`
|
||||
|
||||
## How to run the test
|
||||
|
||||
When these tests are ran, Azure Data Studio will be launched using new temp folders for installed extensions and data so that your local dev environment won't be changed.
|
||||
|
||||
1. In the build pipeline:
|
||||
The integration test suite has been added to ADS windows pipeline to run the test and report the results, you can find the test results under the test tab.
|
||||
|
||||
2. Local environment:
|
||||
1. Install [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli)
|
||||
1. Close all currently active VS Code windows
|
||||
1. open a terminal window/command line window
|
||||
1. navigate to this folder and then run 'node setEnvironmentVariables.js', there are different options, by default VSCode will be opened.
|
||||
1. Terminal(Mac)/CMD(Windows): node setEnvironmentVariables.js Terminal
|
||||
2. Git-Bash on Windows: node setEnvironmentVariables.js BashWin
|
||||
1. Follow the instructions in the window: you will be prompted to login to azure portal.
|
||||
1. Open a terminal window/command line window
|
||||
1. Run `az login` to login with your Microsoft AAD account.
|
||||
1. Navigate to this folder and then run `node setEnvironmentVariables.js`, there are different options, by default VS Code will be opened.
|
||||
1. Terminal(Mac)/CMD(Windows): `node setEnvironmentVariables.js Terminal`
|
||||
2. Git-Bash on Windows: `node setEnvironmentVariables.js BashWin`
|
||||
1. A new window will be opened based on your selection and the new window will have the required environment variables set.
|
||||
1. Run the Test:
|
||||
1. For Integration Test: in the new window navigate to the scripts folder and run sql-test-integration.bat or sql-test-integration.sh based on your environment.
|
||||
2. Smoke Test can be launched in 2 ways:
|
||||
1. In the new window navigate to the test/smoke folder and run: node smoke/index.js
|
||||
2. Or, In a VSCode window opened by step above, open AzureDataStudio folder and then select the 'Launch Smoke Test' option.
|
||||
2. In the new window navigate to the scripts folder and run sql-test-integration.[bat|sh]
|
||||
|
||||
## Skipping Python Installation Tests
|
||||
|
||||
The integration tests contain some tests that test the Python installation for Notebooks. This can take a long time to run and so if you do not need to run them you can skip them by setting the `SKIP_PYTHON_INSTALL_TEST` environment variable to `1`
|
||||
|
||||
## How to debug the tests
|
||||
1. Set the debug target to `Attach to Extension Host`
|
||||
@@ -38,6 +40,6 @@ The integration test suite has been added to ADS windows pipeline to run the tes
|
||||
|
||||
## Code Coverage
|
||||
|
||||
Code coverage for these tests is enabled by default. After running the tests you can find the results in the `coverage` folder at the root of this extension.
|
||||
Code coverage is enabled by default. After running the tests you can find the results in the `coverage` folder at the root of this extension.
|
||||
|
||||
This code coverage covers extension code only - it will not instrument code from the core.
|
||||
|
||||
@@ -302,7 +302,7 @@ suite('Schema compare integration test suite @DacFx@', () => {
|
||||
assert(includeResult.affectedDependencies[0].included === true, 'Table t2 should be included as a result of including v1. Actual: false');
|
||||
|
||||
// excluding views from the comparison should make it so t2 can be excluded
|
||||
deploymentOptions.excludeObjectTypes.value.push(mssql.SchemaObjectType.Views);
|
||||
deploymentOptions.excludeObjectTypes.value.push(Object.keys(deploymentOptions.objectTypesDictionary).find((key) => { return deploymentOptions.objectTypesDictionary[key] === 'Views'; }));
|
||||
await schemaCompareService.schemaCompare(operationId, source, target, azdata.TaskExecutionMode.execute, deploymentOptions);
|
||||
const excludeResult3 = await schemaCompareService.schemaCompareIncludeExcludeNode(operationId, t2Difference, false, azdata.TaskExecutionMode.execute);
|
||||
assertIncludeExcludeResult(excludeResult3, true, 0, 0);
|
||||
@@ -507,7 +507,7 @@ suite('Schema compare integration test suite @DacFx@', () => {
|
||||
|
||||
const deploymentOptionsResult = await schemaCompareService.schemaCompareGetDefaultOptions();
|
||||
let deploymentOptions = deploymentOptionsResult.defaultDeploymentOptions;
|
||||
deploymentOptions.excludeObjectTypes.value.push(mssql.SchemaObjectType.TableValuedFunctions);
|
||||
deploymentOptions.excludeObjectTypes.value.push(Object.keys(deploymentOptions.objectTypesDictionary).find((key) => { return deploymentOptions.objectTypesDictionary[key] === 'TableValuedFunctions'; }));
|
||||
const schemaCompareResult = await schemaCompareService.schemaCompare(operationId, source, target, azdata.TaskExecutionMode.execute, deploymentOptions);
|
||||
assertSchemaCompareResult(schemaCompareResult, operationId, 3);
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
],
|
||||
"markdown.markdownItPlugins": true,
|
||||
"markdown.previewStyles": [
|
||||
"./node_modules/katex/dist/katex.min.css",
|
||||
"./notebook-out/katex.min.css",
|
||||
"./preview-styles/index.css"
|
||||
],
|
||||
"configuration": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
|
||||
"version": "4.1.0.16",
|
||||
"version": "4.2.1.6",
|
||||
"downloadFileNames": {
|
||||
"Windows_86": "win-x86-net6.0.zip",
|
||||
"Windows_64": "win-x64-net6.0.zip",
|
||||
|
||||
@@ -387,7 +387,7 @@
|
||||
"mssql.parallelMessageProcessing": {
|
||||
"type": "boolean",
|
||||
"description": "%mssql.parallelMessageProcessing%",
|
||||
"default": true
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -511,7 +511,7 @@
|
||||
},
|
||||
{
|
||||
"command": "mssql.newTable",
|
||||
"when": "connectionProvider == MSSQL && nodeType == Folder && nodeLabel == Tables && config.workbench.enablePreviewFeatures",
|
||||
"when": "connectionProvider == MSSQL && nodeType == Folder && objectType == Tables && config.workbench.enablePreviewFeatures",
|
||||
"group": "0_query@1"
|
||||
}
|
||||
],
|
||||
@@ -523,7 +523,7 @@
|
||||
},
|
||||
{
|
||||
"command": "mssql.newTable",
|
||||
"when": "connectionProvider == MSSQL && nodeType == Folder && nodeLabel == Tables && config.workbench.enablePreviewFeatures",
|
||||
"when": "connectionProvider == MSSQL && nodeType == Folder && objectType == Tables && config.workbench.enablePreviewFeatures",
|
||||
"group": "connection@1"
|
||||
}
|
||||
],
|
||||
|
||||
81
extensions/mssql/src/mssql.d.ts
vendored
81
extensions/mssql/src/mssql.d.ts
vendored
@@ -163,10 +163,10 @@ declare module 'mssql' {
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface containing deployment options of integer type, value property holds values from <DacFx>\Product\Source\DeploymentApi\ObjectTypes.cs enum
|
||||
* Interface containing deployment options of string[] type, value property holds enum names (nothing but option name) from <DacFx>\Product\Source\DeploymentApi\ObjectTypes.cs enum
|
||||
*/
|
||||
export interface DacDeployOptionPropertyObject {
|
||||
value: number[];
|
||||
value: string[];
|
||||
description: string;
|
||||
displayName: string;
|
||||
}
|
||||
@@ -179,6 +179,8 @@ declare module 'mssql' {
|
||||
excludeObjectTypes: DacDeployOptionPropertyObject;
|
||||
// key will be the boolean option name
|
||||
booleanOptionsDictionary: { [key: string]: DacDeployOptionPropertyBoolean };
|
||||
// key will be the object type enum name (nothing but option name)
|
||||
objectTypesDictionary: { [key: string]: string };
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -189,81 +191,6 @@ declare module 'mssql' {
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Values from <DacFx>\Product\Source\DeploymentApi\ObjectTypes.cs
|
||||
*/
|
||||
export const enum SchemaObjectType {
|
||||
Aggregates = 0,
|
||||
ApplicationRoles = 1,
|
||||
Assemblies = 2,
|
||||
AssemblyFiles = 3,
|
||||
AsymmetricKeys = 4,
|
||||
BrokerPriorities = 5,
|
||||
Certificates = 6,
|
||||
ColumnEncryptionKeys = 7,
|
||||
ColumnMasterKeys = 8,
|
||||
Contracts = 9,
|
||||
DatabaseOptions = 10,
|
||||
DatabaseRoles = 11,
|
||||
DatabaseTriggers = 12,
|
||||
Defaults = 13,
|
||||
ExtendedProperties = 14,
|
||||
ExternalDataSources = 15,
|
||||
ExternalFileFormats = 16,
|
||||
ExternalTables = 17,
|
||||
Filegroups = 18,
|
||||
Files = 19,
|
||||
FileTables = 20,
|
||||
FullTextCatalogs = 21,
|
||||
FullTextStoplists = 22,
|
||||
MessageTypes = 23,
|
||||
PartitionFunctions = 24,
|
||||
PartitionSchemes = 25,
|
||||
Permissions = 26,
|
||||
Queues = 27,
|
||||
RemoteServiceBindings = 28,
|
||||
RoleMembership = 29,
|
||||
Rules = 30,
|
||||
ScalarValuedFunctions = 31,
|
||||
SearchPropertyLists = 32,
|
||||
SecurityPolicies = 33,
|
||||
Sequences = 34,
|
||||
Services = 35,
|
||||
Signatures = 36,
|
||||
StoredProcedures = 37,
|
||||
SymmetricKeys = 38,
|
||||
Synonyms = 39,
|
||||
Tables = 40,
|
||||
TableValuedFunctions = 41,
|
||||
UserDefinedDataTypes = 42,
|
||||
UserDefinedTableTypes = 43,
|
||||
ClrUserDefinedTypes = 44,
|
||||
Users = 45,
|
||||
Views = 46,
|
||||
XmlSchemaCollections = 47,
|
||||
Audits = 48,
|
||||
Credentials = 49,
|
||||
CryptographicProviders = 50,
|
||||
DatabaseAuditSpecifications = 51,
|
||||
DatabaseEncryptionKeys = 52,
|
||||
DatabaseScopedCredentials = 53,
|
||||
Endpoints = 54,
|
||||
ErrorMessages = 55,
|
||||
EventNotifications = 56,
|
||||
EventSessions = 57,
|
||||
LinkedServerLogins = 58,
|
||||
LinkedServers = 59,
|
||||
Logins = 60,
|
||||
MasterKeys = 61,
|
||||
Routes = 62,
|
||||
ServerAuditSpecifications = 63,
|
||||
ServerRoleMembership = 64,
|
||||
ServerRoles = 65,
|
||||
ServerTriggers = 66,
|
||||
ExternalStreams = 67,
|
||||
ExternalStreamingJobs = 68
|
||||
}
|
||||
|
||||
export interface SchemaCompareObjectId {
|
||||
nameParts: string[];
|
||||
sqlObjectType: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import * as constants from './../common/constants';
|
||||
import { BookTreeItem } from './bookTreeItem';
|
||||
import { getPinnedNotebooks, setPinnedBookPathsInConfig, IPinnedNotebook } from '../common/utils';
|
||||
import { getPinnedNotebooks, setPinnedBookPathsInConfig, IPinnedNotebook, getNotebookType } from '../common/utils';
|
||||
|
||||
export interface IBookPinManager {
|
||||
pinNotebook(notebook: BookTreeItem): Promise<boolean>;
|
||||
@@ -58,7 +58,7 @@ export class BookPinManager implements IBookPinManager {
|
||||
pinnedBooks.splice(existingBookIndex, 1);
|
||||
modifiedPinnedBooks = true;
|
||||
} else if (existingBookIndex === -1 && operation === PinBookOperation.Pin) {
|
||||
let addNotebook: IPinnedNotebook = { notebookPath: bookPathToChange, bookPath: notebook.book.root, title: notebook.book.title };
|
||||
let addNotebook: IPinnedNotebook = { notebookPath: bookPathToChange, bookPath: getNotebookType(notebook.book) ? notebook.book.root : undefined, title: notebook.book.title };
|
||||
pinnedBooks.push(addNotebook);
|
||||
modifiedPinnedBooks = true;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Deferred } from '../common/promise';
|
||||
import { IBookTrustManager, BookTrustManager } from './bookTrustManager';
|
||||
import * as loc from '../common/localizedConstants';
|
||||
import * as glob from 'fast-glob';
|
||||
import { getPinnedNotebooks, confirmMessageDialog, getNotebookType, FileExtension, IPinnedNotebook, BookTreeItemType } from '../common/utils';
|
||||
import { getPinnedNotebooks, getNotebookType, confirmMessageDialog, FileExtension, IPinnedNotebook, BookTreeItemType } from '../common/utils';
|
||||
import { IBookPinManager, BookPinManager } from './bookPinManager';
|
||||
import { BookTocManager, IBookTocManager, quickPickResults } from './bookTocManager';
|
||||
import { CreateBookDialog } from '../dialog/createBookDialog';
|
||||
@@ -138,7 +138,8 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
||||
let pinStatusChanged = await this.bookPinManager.pinNotebook(bookTreeItem);
|
||||
sendNotebookActionEvent(NbTelemetryView.Book, NbTelemetryAction.PinNotebook);
|
||||
if (pinStatusChanged) {
|
||||
bookTreeItem.contextValue = 'pinnedNotebook';
|
||||
bookTreeItem.contextValue = BookTreeItemType.pinnedNotebook;
|
||||
this._onDidChangeTreeData.fire(bookTreeItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,7 +149,16 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
||||
if (bookPathToUpdate) {
|
||||
let pinStatusChanged = await this.bookPinManager.unpinNotebook(bookTreeItem);
|
||||
if (pinStatusChanged) {
|
||||
bookTreeItem.contextValue = getNotebookType(bookTreeItem.book);
|
||||
// reset to original context value
|
||||
bookTreeItem.contextValue = bookTreeItem.book.type === BookTreeItemType.Markdown ? BookTreeItemType.Markdown : getNotebookType(bookTreeItem.book);
|
||||
// to search for notebook in allNotebooks dictionary we need to format uri
|
||||
const notebookUri = vscode.Uri.file(bookTreeItem.book.contentPath).fsPath;
|
||||
// if notebook is not in current book then it is a standalone notebook
|
||||
let itemOpenedInBookTreeView = this.currentBook?.getNotebook(notebookUri) ?? this.books.find(book => book.bookPath === bookTreeItem.book.contentPath)?.getNotebook(notebookUri);
|
||||
if (itemOpenedInBookTreeView) {
|
||||
itemOpenedInBookTreeView.contextValue = bookTreeItem.contextValue;
|
||||
this._onDidChangeTreeData.fire(itemOpenedInBookTreeView.parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,7 +274,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
||||
async addNotebookToPinnedView(bookItem: BookTreeItem): Promise<void> {
|
||||
let notebookPath: string = bookItem.book.contentPath;
|
||||
if (notebookPath) {
|
||||
let notebookDetails: IPinnedNotebook = bookItem.book.root ? { bookPath: bookItem.book.root, notebookPath: notebookPath, title: bookItem.book.title } : { notebookPath: notebookPath };
|
||||
let notebookDetails: IPinnedNotebook = getNotebookType(bookItem.book) === BookTreeItemType.savedBookNotebook ? { bookPath: bookItem.book.root, notebookPath: notebookPath, title: bookItem.book.title } : { notebookPath: notebookPath };
|
||||
await this.createAndAddBookModel(notebookPath, true, notebookDetails);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M15.4,4.9a8.3,8.3,0,0,1,0,6.2,9.009,9.009,0,0,1-1.7,2.6,9.009,9.009,0,0,1-2.6,1.7A8.112,8.112,0,0,1,8,16a7.509,7.509,0,0,1-2.6-.4,7.609,7.609,0,0,1-2.3-1.3,7.31,7.31,0,0,1-1.7-1.8L.7,11.4c-.1-.4-.3-.8-.4-1.3l1-.2a7.207,7.207,0,0,0,.9,2,8.716,8.716,0,0,0,1.6,1.7,6.9,6.9,0,0,0,1.9,1A6.184,6.184,0,0,0,8,15l1.9-.2,1.6-.8a4.9,4.9,0,0,0,1.4-1.1A4.9,4.9,0,0,0,14,11.5a7.976,7.976,0,0,0,.8-1.6A12.233,12.233,0,0,0,15,8a12.233,12.233,0,0,0-.2-1.9A7.976,7.976,0,0,0,14,4.5a4.9,4.9,0,0,0-1.1-1.4A4.9,4.9,0,0,0,11.5,2a4.61,4.61,0,0,0-1.6-.7A6.283,6.283,0,0,0,8,1a6.879,6.879,0,0,0-2,.3,5.292,5.292,0,0,0-1.7.8A4.708,4.708,0,0,0,2.8,3.4,4.6,4.6,0,0,0,1.7,5H4V6H0V2H1V4.1l.3-.4.3-.5A9.122,9.122,0,0,1,3.3,1.5,7.6,7.6,0,0,1,5.5.4,7.308,7.308,0,0,1,8,0a8.112,8.112,0,0,1,3.1.6,9.009,9.009,0,0,1,2.6,1.7A9.009,9.009,0,0,1,15.4,4.9Z" />
|
||||
<polygon points="8 3 8 7.3 10.9 10.1 10.1 10.9 7 7.7 7 3 8 3" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1002 B |
@@ -2,7 +2,7 @@
|
||||
"name": "query-history",
|
||||
"displayName": "%queryHistory.displayName%",
|
||||
"description": "%queryHistory.description%",
|
||||
"version": "0.2.0",
|
||||
"version": "0.4.0",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
@@ -39,6 +39,19 @@
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "%queryHistory.captureEnabledDesc%"
|
||||
},
|
||||
"queryHistory.doubleClickAction": {
|
||||
"type": "string",
|
||||
"description": "%queryHistory.doubleClickAction%",
|
||||
"default": "open",
|
||||
"enum": [
|
||||
"open",
|
||||
"run"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"%queryHistory.doubleClickAction.open%",
|
||||
"%queryHistory.doubleClickAction.run%"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,7 +183,7 @@
|
||||
{
|
||||
"id": "queryHistory",
|
||||
"title": "%queryHistory.displayName%",
|
||||
"icon": "./images/history.png"
|
||||
"icon": "$(history)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"queryHistory.displayName": "Query History",
|
||||
"queryHistory.description": "View and run previously executed queries",
|
||||
"queryHistory.captureEnabledDesc": "Whether Query History capture is enabled. If false queries executed will not be captured.",
|
||||
"queryHistory.doubleClickAction": "The action taken when a history item is double clicked",
|
||||
"queryHistory.doubleClickAction.open": "Open a new disconnected editor with the query from the selected history item",
|
||||
"queryHistory.doubleClickAction.run": "Open a new connected editor with the query and connection from the selected history item and automatically run the query",
|
||||
"queryHistory.open": "Open Query",
|
||||
"queryHistory.run": "Run Query",
|
||||
"queryHistory.delete": "Delete",
|
||||
|
||||
10
extensions/query-history/src/constants.ts
Normal file
10
extensions/query-history/src/constants.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const QUERY_HISTORY_CONFIG_SECTION = 'queryHistory';
|
||||
export const CAPTURE_ENABLED_CONFIG_SECTION = 'captureEnabled';
|
||||
export const DOUBLE_CLICK_ACTION_CONFIG_SECTION = 'doubleClickAction';
|
||||
|
||||
export const ITEM_SELECTED_COMMAND_ID = 'queryHistory.itemSelected';
|
||||
@@ -5,37 +5,89 @@
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { QueryHistoryNode } from './queryHistoryNode';
|
||||
import { DOUBLE_CLICK_ACTION_CONFIG_SECTION, ITEM_SELECTED_COMMAND_ID, QUERY_HISTORY_CONFIG_SECTION } from './constants';
|
||||
import { QueryHistoryItem } from './queryHistoryItem';
|
||||
import { QueryHistoryProvider } from './queryHistoryProvider';
|
||||
|
||||
let lastSelectedItem: { item: QueryHistoryItem | undefined, time: number | undefined } = {
|
||||
item: undefined,
|
||||
time: undefined
|
||||
};
|
||||
/**
|
||||
* The time in ms between clicks to count as a double click on our tree view items
|
||||
*/
|
||||
const DOUBLE_CLICK_TIMEOUT_MS = 500;
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext): Promise<void> {
|
||||
const provider = new QueryHistoryProvider();
|
||||
context.subscriptions.push(provider);
|
||||
context.subscriptions.push(vscode.window.registerTreeDataProvider('queryHistory', provider));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.open', async (node: QueryHistoryNode) => {
|
||||
return azdata.queryeditor.openQueryDocument(
|
||||
{
|
||||
content: node.queryText
|
||||
}, node.connectionProfile?.providerId);
|
||||
const treeDataProvider = new QueryHistoryProvider();
|
||||
context.subscriptions.push(treeDataProvider);
|
||||
const treeView = vscode.window.createTreeView('queryHistory', {
|
||||
treeDataProvider,
|
||||
canSelectMany: false
|
||||
});
|
||||
context.subscriptions.push(treeView);
|
||||
// This is an internal-only command so not adding to package.json
|
||||
context.subscriptions.push(vscode.commands.registerCommand(ITEM_SELECTED_COMMAND_ID, async (selectedItem: QueryHistoryItem) => {
|
||||
// VS Code doesn't provide a native way to detect a double-click so we track it ourselves by keeping track of the last item clicked and
|
||||
// when it was clicked to compare, then if a click happens on the same element quickly enough we trigger the configured action
|
||||
const clickTime = new Date().getTime();
|
||||
if (lastSelectedItem.item === selectedItem && lastSelectedItem.time && (clickTime - lastSelectedItem.time) < DOUBLE_CLICK_TIMEOUT_MS) {
|
||||
const doubleClickAction = vscode.workspace.getConfiguration(QUERY_HISTORY_CONFIG_SECTION).get<string>(DOUBLE_CLICK_ACTION_CONFIG_SECTION);
|
||||
switch (doubleClickAction) {
|
||||
case 'run':
|
||||
await runQuery(selectedItem);
|
||||
break;
|
||||
case 'open':
|
||||
default:
|
||||
await openQuery(selectedItem);
|
||||
break;
|
||||
}
|
||||
// Clear out the last selected item so we don't run the command again on a 3rd click
|
||||
lastSelectedItem = {
|
||||
item: undefined,
|
||||
time: undefined
|
||||
};
|
||||
} else {
|
||||
// Update the last selected item since we didn't run a command
|
||||
lastSelectedItem = {
|
||||
item: selectedItem,
|
||||
time: clickTime
|
||||
};
|
||||
}
|
||||
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.run', async (node: QueryHistoryNode) => {
|
||||
const doc = await azdata.queryeditor.openQueryDocument(
|
||||
{
|
||||
content: node.queryText
|
||||
}, node.connectionProfile?.providerId);
|
||||
await azdata.queryeditor.connect(doc.uri, node.connectionProfile?.connectionId || '');
|
||||
azdata.queryeditor.runQuery(doc.uri);
|
||||
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.open', async (item: QueryHistoryItem) => {
|
||||
return openQuery(item);
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.delete', (node: QueryHistoryNode) => {
|
||||
provider.deleteNode(node);
|
||||
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.run', async (item: QueryHistoryItem) => {
|
||||
return runQuery(item);
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.delete', (item: QueryHistoryItem) => {
|
||||
treeDataProvider.deleteItem(item);
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.clear', () => {
|
||||
provider.clearAll();
|
||||
treeDataProvider.clearAll();
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.disableCapture', async () => {
|
||||
return provider.setCaptureEnabled(false);
|
||||
return treeDataProvider.setCaptureEnabled(false);
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('queryHistory.enableCapture', async () => {
|
||||
return provider.setCaptureEnabled(true);
|
||||
return treeDataProvider.setCaptureEnabled(true);
|
||||
}));
|
||||
}
|
||||
|
||||
async function openQuery(item: QueryHistoryItem): Promise<void> {
|
||||
await azdata.queryeditor.openQueryDocument(
|
||||
{
|
||||
content: item.queryText
|
||||
}, item.connectionProfile?.providerId);
|
||||
}
|
||||
|
||||
async function runQuery(item: QueryHistoryItem): Promise<void> {
|
||||
const doc = await azdata.queryeditor.openQueryDocument(
|
||||
{
|
||||
content: item.queryText
|
||||
}, item.connectionProfile?.providerId);
|
||||
await azdata.queryeditor.connect(doc.uri, item.connectionProfile?.connectionId || '');
|
||||
azdata.queryeditor.runQuery(doc.uri);
|
||||
}
|
||||
|
||||
13
extensions/query-history/src/queryHistoryItem.ts
Normal file
13
extensions/query-history/src/queryHistoryItem.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
export interface QueryHistoryItem {
|
||||
readonly queryText: string,
|
||||
readonly connectionProfile: azdata.connection.ConnectionProfile | undefined,
|
||||
readonly timestamp: Date,
|
||||
readonly isSuccess: boolean
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { removeNewLines } from './utils';
|
||||
|
||||
export class QueryHistoryNode extends vscode.TreeItem {
|
||||
constructor(
|
||||
public readonly queryText: string,
|
||||
public readonly connectionProfile: azdata.connection.ConnectionProfile | undefined,
|
||||
timestamp: Date,
|
||||
isSuccess: boolean
|
||||
) {
|
||||
super(removeNewLines(queryText), vscode.TreeItemCollapsibleState.None);
|
||||
this.iconPath = isSuccess ? new vscode.ThemeIcon('check', new vscode.ThemeColor('testing.iconPassed')) : new vscode.ThemeIcon('error', new vscode.ThemeColor('testing.iconFailed'));
|
||||
this.tooltip = queryText;
|
||||
this.description = connectionProfile ? `${connectionProfile.serverName}|${connectionProfile.databaseName} ${timestamp.toLocaleString()}` : timestamp.toLocaleString();
|
||||
}
|
||||
}
|
||||
@@ -6,18 +6,20 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import { EOL } from 'os';
|
||||
import { QueryHistoryNode } from './queryHistoryNode';
|
||||
import { QueryHistoryItem } from './queryHistoryItem';
|
||||
import { removeNewLines } from './utils';
|
||||
import { CAPTURE_ENABLED_CONFIG_SECTION, ITEM_SELECTED_COMMAND_ID, QUERY_HISTORY_CONFIG_SECTION } from './constants';
|
||||
|
||||
const QUERY_HISTORY_CONFIG_SECTION = 'queryHistory';
|
||||
const CAPTURE_ENABLED_CONFIG_SECTION = 'captureEnabled';
|
||||
const DEFAULT_CAPTURE_ENABLED = true;
|
||||
const successIcon = new vscode.ThemeIcon('check', new vscode.ThemeColor('testing.iconPassed'));
|
||||
const failedIcon = new vscode.ThemeIcon('error', new vscode.ThemeColor('testing.iconFailed'));
|
||||
|
||||
export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistoryNode>, vscode.Disposable {
|
||||
export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistoryItem>, vscode.Disposable {
|
||||
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<QueryHistoryNode | undefined> = new vscode.EventEmitter<QueryHistoryNode | undefined>();
|
||||
readonly onDidChangeTreeData: vscode.Event<QueryHistoryNode | undefined> = this._onDidChangeTreeData.event;
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<QueryHistoryItem | undefined> = new vscode.EventEmitter<QueryHistoryItem | undefined>();
|
||||
readonly onDidChangeTreeData: vscode.Event<QueryHistoryItem | undefined> = this._onDidChangeTreeData.event;
|
||||
|
||||
private _queryHistoryNodes: QueryHistoryNode[] = [];
|
||||
private _queryHistoryItems: QueryHistoryItem[] = [];
|
||||
private _captureEnabled: boolean = true;
|
||||
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
@@ -37,10 +39,10 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
|
||||
}
|
||||
// Combine all the text from the batches back together
|
||||
const queryText = queryInfo.batchRanges.map(r => textDocument.getText(r) ?? '').join(EOL);
|
||||
const connProfile = await azdata.connection.getConnection(document.uri);
|
||||
const isError = queryInfo.messages.find(m => m.isError) ? false : true;
|
||||
const connectionProfile = await azdata.connection.getConnection(document.uri);
|
||||
const isSuccess = queryInfo.messages.find(m => m.isError) ? false : true;
|
||||
// Add to the front of the list so the new item appears at the top
|
||||
this._queryHistoryNodes.unshift(new QueryHistoryNode(queryText, connProfile, new Date(), isError));
|
||||
this._queryHistoryItems.unshift({ queryText, connectionProfile, timestamp: new Date(), isSuccess });
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
}
|
||||
}
|
||||
@@ -54,21 +56,26 @@ export class QueryHistoryProvider implements vscode.TreeDataProvider<QueryHistor
|
||||
}
|
||||
|
||||
public clearAll(): void {
|
||||
this._queryHistoryNodes = [];
|
||||
this._queryHistoryItems = [];
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
}
|
||||
|
||||
public deleteNode(node: QueryHistoryNode): void {
|
||||
this._queryHistoryNodes = this._queryHistoryNodes.filter(n => n !== node);
|
||||
public deleteItem(item: QueryHistoryItem): void {
|
||||
this._queryHistoryItems = this._queryHistoryItems.filter(n => n !== item);
|
||||
this._onDidChangeTreeData.fire(undefined);
|
||||
}
|
||||
public getTreeItem(node: QueryHistoryNode): vscode.TreeItem {
|
||||
return node;
|
||||
public getTreeItem(item: QueryHistoryItem): vscode.TreeItem {
|
||||
const treeItem = new vscode.TreeItem(removeNewLines(item.queryText), vscode.TreeItemCollapsibleState.None);
|
||||
treeItem.iconPath = item.isSuccess ? successIcon : failedIcon;
|
||||
treeItem.tooltip = item.queryText;
|
||||
treeItem.description = item.connectionProfile ? `${item.connectionProfile.serverName}|${item.connectionProfile.databaseName} ${item.timestamp.toLocaleString()}` : item.timestamp.toLocaleString();
|
||||
treeItem.command = { title: '', command: ITEM_SELECTED_COMMAND_ID, arguments: [item] };
|
||||
return treeItem;
|
||||
}
|
||||
|
||||
public getChildren(element?: QueryHistoryNode): QueryHistoryNode[] {
|
||||
public getChildren(element?: QueryHistoryItem): QueryHistoryItem[] {
|
||||
// We only have top level items
|
||||
return this._queryHistoryNodes;
|
||||
return this._queryHistoryItems;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'mocha';
|
||||
import * as sinon from 'sinon';
|
||||
import * as azdataTest from '@microsoft/azdata-test';
|
||||
import { QueryHistoryProvider } from '../queryHistoryProvider';
|
||||
import { QueryHistoryNode } from '../queryHistoryNode';
|
||||
import { QueryHistoryItem } from '../queryHistoryItem';
|
||||
import { EOL } from 'os';
|
||||
|
||||
describe('QueryHistoryProvider', () => {
|
||||
@@ -77,7 +77,7 @@ describe('QueryHistoryProvider', () => {
|
||||
});
|
||||
const children = testProvider.getChildren();
|
||||
should(children).length(1, 'Should have one child after adding item');
|
||||
should(children[0].queryText).be.equal(`${rangeWithContent1.content}${EOL}${rangeWithContent2.content}`, 'node content should be combined from both source ranges');
|
||||
should(children[0].queryText).be.equal(`${rangeWithContent1.content}${EOL}${rangeWithContent2.content}`, 'item content should be combined from both source ranges');
|
||||
});
|
||||
|
||||
it('event with errors is marked as error', async function () {
|
||||
@@ -87,7 +87,7 @@ describe('QueryHistoryProvider', () => {
|
||||
await fireQueryEventAndWaitForRefresh('queryStop', <any>{ uri: testUri.toString() }, { messages: [ message1, message2, message3 ], batchRanges: []});
|
||||
const children = testProvider.getChildren();
|
||||
should(children).length(1, 'Should have one child after adding item');
|
||||
should((<vscode.ThemeIcon>children[0].iconPath).id).be.equal('error', 'Event with errors should have error icon');
|
||||
should(children[0].isSuccess).be.false('Event with errors should have error icon');
|
||||
});
|
||||
|
||||
it('event without errors is marked as success', async function () {
|
||||
@@ -97,12 +97,12 @@ describe('QueryHistoryProvider', () => {
|
||||
await fireQueryEventAndWaitForRefresh('queryStop', <any>{ uri: testUri.toString() }, { messages: [ message1, message2, message3 ], batchRanges: []});
|
||||
const children = testProvider.getChildren();
|
||||
should(children).length(1, 'Should have one child after adding item');
|
||||
should((<vscode.ThemeIcon>children[0].iconPath).id).be.equal('check', 'Event without errors should have check icon');
|
||||
should(children[0].isSuccess).be.true('Event without errors should have check icon');
|
||||
});
|
||||
|
||||
it('queryStop events from unknown document are ignored', async function () {
|
||||
const unknownUri = vscode.Uri.parse('untitled://query2');
|
||||
// Since we didn't find the text document we'll never update the node list so add a timeout since that event will never fire
|
||||
// Since we didn't find the text document we'll never update the item list so add a timeout since that event will never fire
|
||||
await fireQueryEventAndWaitForRefresh('queryStop', <any>{ uri: unknownUri.toString() }, { messages: [], batchRanges: [] }, 2000);
|
||||
const children = testProvider.getChildren();
|
||||
should(children).length(0, 'Should not have any children');
|
||||
@@ -113,7 +113,7 @@ describe('QueryHistoryProvider', () => {
|
||||
let children = testProvider.getChildren();
|
||||
should(children).length(1, 'Should have one child after adding item');
|
||||
|
||||
await waitForNodeRefresh(() => testProvider.clearAll());
|
||||
await waitForItemRefresh(() => testProvider.clearAll());
|
||||
children = testProvider.getChildren();
|
||||
should(children).length(0, 'Should have no children after clearing');
|
||||
});
|
||||
@@ -125,53 +125,53 @@ describe('QueryHistoryProvider', () => {
|
||||
let children = testProvider.getChildren();
|
||||
should(children).length(3, 'Should have 3 children after adding item');
|
||||
|
||||
await waitForNodeRefresh(() => testProvider.clearAll());
|
||||
await waitForItemRefresh(() => testProvider.clearAll());
|
||||
children = testProvider.getChildren();
|
||||
should(children).length(0, 'Should have no children after clearing');
|
||||
});
|
||||
|
||||
it('delete node when no nodes doesn\'t throw', async function () {
|
||||
const testNode: QueryHistoryNode = { queryText: 'SELECT 1', connectionProfile: azdataTest.stubs.connectionProfile.createConnectionProfile() };
|
||||
await waitForNodeRefresh(() => testProvider.deleteNode(testNode));
|
||||
it('delete item when no items doesn\'t throw', async function () {
|
||||
const testItem: QueryHistoryItem = { queryText: 'SELECT 1', connectionProfile: azdataTest.stubs.connectionProfile.createConnectionProfile(), timestamp: new Date(), isSuccess: true };
|
||||
await waitForItemRefresh(() => testProvider.deleteItem(testItem));
|
||||
const children = testProvider.getChildren();
|
||||
should(children).length(0, 'Should have no children after deleting node');
|
||||
should(children).length(0, 'Should have no children after deleting item');
|
||||
});
|
||||
|
||||
it('delete node that doesn\'t exist doesn\'t throw', async function () {
|
||||
it('delete item that doesn\'t exist doesn\'t throw', async function () {
|
||||
await fireQueryEventAndWaitForRefresh('queryStop', <any>{ uri: testUri.toString() }, { messages: [], batchRanges: [] });
|
||||
let children = testProvider.getChildren();
|
||||
should(children).length(1, 'Should have 1 child initially');
|
||||
|
||||
const testNode: QueryHistoryNode = { queryText: 'SELECT 1', connectionProfile: azdataTest.stubs.connectionProfile.createConnectionProfile() };
|
||||
await waitForNodeRefresh(() => testProvider.deleteNode(testNode));
|
||||
const testItem: QueryHistoryItem = { queryText: 'SELECT 1', connectionProfile: azdataTest.stubs.connectionProfile.createConnectionProfile(), timestamp: new Date(), isSuccess: true };
|
||||
await waitForItemRefresh(() => testProvider.deleteItem(testItem));
|
||||
children = testProvider.getChildren();
|
||||
should(children).length(1, 'Should still have 1 child after deleting node');
|
||||
should(children).length(1, 'Should still have 1 child after deleting item');
|
||||
});
|
||||
|
||||
it('can delete single node', async function () {
|
||||
it('can delete single item', async function () {
|
||||
await fireQueryEventAndWaitForRefresh('queryStop', <any>{ uri: testUri.toString() }, { messages: [], batchRanges: [] });
|
||||
await fireQueryEventAndWaitForRefresh('queryStop', <any>{ uri: testUri.toString() }, { messages: [], batchRanges: [] });
|
||||
await fireQueryEventAndWaitForRefresh('queryStop', <any>{ uri: testUri.toString() }, { messages: [], batchRanges: [] });
|
||||
const firstChildren = testProvider.getChildren();
|
||||
should(firstChildren).length(3, 'Should have 3 children initially');
|
||||
|
||||
let nodeToDelete: QueryHistoryNode = firstChildren[1];
|
||||
await waitForNodeRefresh(() => testProvider.deleteNode(nodeToDelete));
|
||||
let itemToDelete: QueryHistoryItem = firstChildren[1];
|
||||
await waitForItemRefresh(() => testProvider.deleteItem(itemToDelete));
|
||||
const secondChildren = testProvider.getChildren();
|
||||
should(secondChildren).length(2, 'Should still have 2 child after deleting node');
|
||||
should(secondChildren[0]).be.equal(firstChildren[0], 'First node should still exist after deleting first node');
|
||||
should(secondChildren[1]).be.equal(firstChildren[2], 'Second node should still exist after deleting first node');
|
||||
should(secondChildren).length(2, 'Should still have 2 child after deleting item');
|
||||
should(secondChildren[0]).be.equal(firstChildren[0], 'First item should still exist after deleting first item');
|
||||
should(secondChildren[1]).be.equal(firstChildren[2], 'Second item should still exist after deleting first item');
|
||||
|
||||
nodeToDelete = secondChildren[0];
|
||||
await waitForNodeRefresh(() => testProvider.deleteNode(nodeToDelete));
|
||||
itemToDelete = secondChildren[0];
|
||||
await waitForItemRefresh(() => testProvider.deleteItem(itemToDelete));
|
||||
const thirdChildren = testProvider.getChildren();
|
||||
should(thirdChildren).length(1, 'Should still have 1 child after deleting node');
|
||||
should(thirdChildren[0]).be.equal(secondChildren[1], 'Second node should still exist after deleting second node');
|
||||
should(thirdChildren).length(1, 'Should still have 1 child after deleting item');
|
||||
should(thirdChildren[0]).be.equal(secondChildren[1], 'Second item should still exist after deleting second item');
|
||||
|
||||
nodeToDelete = thirdChildren[0];
|
||||
await waitForNodeRefresh(() => testProvider.deleteNode(nodeToDelete));
|
||||
itemToDelete = thirdChildren[0];
|
||||
await waitForItemRefresh(() => testProvider.deleteItem(itemToDelete));
|
||||
const fourthChildren = testProvider.getChildren();
|
||||
should(fourthChildren).length(0, 'Should have no children after deleting all nodes');
|
||||
should(fourthChildren).length(0, 'Should have no children after deleting all items');
|
||||
});
|
||||
|
||||
it('pausing capture causes children not to be added', async function () {
|
||||
@@ -192,10 +192,10 @@ describe('QueryHistoryProvider', () => {
|
||||
});
|
||||
|
||||
async function fireQueryEventAndWaitForRefresh(type: azdata.queryeditor.QueryEventType, document: azdata.queryeditor.QueryDocument, queryInfo: azdata.queryeditor.QueryInfo, timeoutMs?: number): Promise<void> {
|
||||
await waitForNodeRefresh(() => testListener.onQueryEvent(type, document, undefined, queryInfo), timeoutMs);
|
||||
await waitForItemRefresh(() => testListener.onQueryEvent(type, document, undefined, queryInfo), timeoutMs);
|
||||
}
|
||||
|
||||
async function waitForNodeRefresh(func: Function, timeoutMs?: number): Promise<void> {
|
||||
async function waitForItemRefresh(func: Function, timeoutMs?: number): Promise<void> {
|
||||
const promises: Promise<any>[] = [azdataTest.helpers.eventToPromise(testProvider.onDidChangeTreeData)];
|
||||
const timeoutPromise = timeoutMs ? new Promise<void>(r => setTimeout(() => r(), timeoutMs)) : undefined;
|
||||
if (timeoutPromise) {
|
||||
|
||||
@@ -202,7 +202,7 @@ This defines the set of options for this field to display. There are a number of
|
||||
|
||||
* String array (`string[]`) - A static list of values that will be shown as a dropdown. Default value selected is defined as `FieldInfo.defaultValue`.
|
||||
|
||||
* CategoryValue array (`azdata.CategoryValue[]`) - A static list of CategoryValue objects that will be shown as a dropdown. Each value will define a display name separate from its value - use this for values you want to display differently to the user (such as names for an Azure region).
|
||||
* CategoryValue array (`azdata.CategoryValue[]`) - A static list of CategoryValue objects that will be shown as a dropdown. Each value will define a display name separate from its value - use this for values you want to display differently to the user (such as names for an Azure region). If you use a CategoryValue array as your options, ensure you set the defaultValue to the CategoryValue's displayName rather than the name.
|
||||
|
||||
* [OptionsInfo](#optionsinfo) - An object allowing more control over the option values.
|
||||
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "e5fb2be9-e904-4821-8473-b69b90760c6a"
|
||||
},
|
||||
"source": [
|
||||
"\n",
|
||||
"## Run SQL Server 2022 Preview container image with Docker\n",
|
||||
"This notebook will use Docker to pull and run the SQL Server 2022 Preview container image and connect to it in Azure Data Studio\n",
|
||||
"\n",
|
||||
"### Dependencies\n",
|
||||
"- Docker Engine. For more information, see [Install Docker](https://docs.docker.com/engine/installation/).\n",
|
||||
"\n",
|
||||
"<span style=\"color:red\"><font size=\"3\">Please press the \"Run all\" button to run the notebook</font></span>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "76c571ab-358a-4b07-810c-53020ee1745a"
|
||||
},
|
||||
"source": [
|
||||
"### Check dependencies"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "6196300e-f896-489b-8dca-b2c42eda2d6d",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import sys,os,getpass,json,html,time\n",
|
||||
"from string import Template\n",
|
||||
"\n",
|
||||
"def run_command(displayCommand = \"\"):\n",
|
||||
" print(\"Executing: \" + displayCommand if displayCommand != \"\" else cmd)\n",
|
||||
" !{cmd}\n",
|
||||
" if _exit_code != 0:\n",
|
||||
" sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n')\n",
|
||||
" print(f'Command successfully executed')\n",
|
||||
"\n",
|
||||
"cmd = 'docker version'\n",
|
||||
"run_command()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "87b07614-d57d-4731-ac3e-a8b324d231f2"
|
||||
},
|
||||
"source": [
|
||||
"### List existing containers\n",
|
||||
"You can view the ports that have been used by existing containers"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "26170d1b-4332-4383-bcc4-1d97030daffc",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"cmd = f'docker ps -a'\n",
|
||||
"run_command()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "52b1faf2-d7c7-446b-ba0b-4f8b744da0bb"
|
||||
},
|
||||
"source": [
|
||||
"### Required information"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "93cb0147-7bf6-4630-b796-3811dfd1354b",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"env_var_flag = \"AZDATA_NB_VAR_DOCKER_PASSWORD\" in os.environ\n",
|
||||
"password_name = 'SQL Server sa account password'\n",
|
||||
"if env_var_flag:\n",
|
||||
" sql_password = os.environ[\"AZDATA_NB_VAR_DOCKER_PASSWORD\"]\n",
|
||||
" sql_port = os.environ[\"AZDATA_NB_VAR_DOCKER_PORT\"]\n",
|
||||
"else:\n",
|
||||
" sql_password = getpass.getpass(prompt = password_name)\n",
|
||||
" password_confirm = getpass.getpass(prompt = f'Confirm {password_name}')\n",
|
||||
" if sql_password != password_confirm:\n",
|
||||
" raise SystemExit(f'{password_name} does not match the confirmation password.')\n",
|
||||
" sql_port = input('SQL Server port, default value is 1433')\n",
|
||||
" if len(sql_port) == 0:\n",
|
||||
" sql_port = '1433'\n",
|
||||
"print(f'{password_name}: ******')\n",
|
||||
"print(f'Port: {sql_port}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "643ccaca-fd1d-4482-b81e-aee29b627e34"
|
||||
},
|
||||
"source": [
|
||||
"### Pull the container image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "7b102447-3198-488f-a995-982ae1fc8555",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"cmd = f'docker pull mcr.microsoft.com/mssql/server:2022-latest'\n",
|
||||
"run_command()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "a4527a5f-c2c5-4f60-bfd1-b119576178c5"
|
||||
},
|
||||
"source": [
|
||||
"### Start a new container"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "82f27460-88eb-4484-92ee-40305e650d70",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"if env_var_flag:\n",
|
||||
" container_name = os.environ[\"AZDATA_NB_VAR_DOCKER_CONTAINER_NAME\"]\n",
|
||||
"else:\n",
|
||||
" container_name = 'sql2022-' + time.strftime(\"%Y%m%d%H%M%S\", time.localtime())\n",
|
||||
"print('New container name: ' + container_name)\n",
|
||||
"\n",
|
||||
"template = Template(f'docker run -e ACCEPT_EULA=Y -e \"SA_PASSWORD=$password\" -p {sql_port}:1433 --name {container_name} -d mcr.microsoft.com/mssql/server:2022-latest')\n",
|
||||
"cmd = template.substitute(password=sql_password)\n",
|
||||
"run_command(template.substitute(password='******'))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "e267aa7d-dd22-43ac-9b03-cf282ef15f67"
|
||||
},
|
||||
"source": [
|
||||
"### List all the containers"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "211ee198-f1d1-4781-9daa-8497c2665de6",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"cmd = f'docker ps -a'\n",
|
||||
"run_command()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "5f5860c4-7962-439e-a15b-7f24f504dc18"
|
||||
},
|
||||
"source": [
|
||||
"### Connect to SQL Server in Azure Data Studio\n",
|
||||
"It might take a couple minutes for SQL Server to launch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "4bc64915-c5ae-4507-8fb0-9e413ccc2fd0",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from IPython.display import *\n",
|
||||
"connectionParameter = '{\"serverName\":\"localhost,' + sql_port + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":\"sa\",\"password\":' + json.dumps(sql_password) + '}'\n",
|
||||
"display(HTML('<br/><a href=\"command:azdata.connect?' + html.escape(connectionParameter)+'\"><font size=\"3\">Click here to connect to SQL Server</font></a><br/>'))\n",
|
||||
"display(HTML('<br/><span style=\"color:red\"><font size=\"2\">NOTE: The SQL Server password is included in this link, you may want to clear the results of this code cell before saving the notebook.</font></span>'))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "9a1039fa-fdd3-408b-b649-8fde0fcee660"
|
||||
},
|
||||
"source": [
|
||||
"### Stop and remove the container"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "f9e0f1ad-ba6e-4c17-84ea-cc5dceb1289b",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"stop_container_command = f'docker stop {container_name}'\n",
|
||||
"remove_container_command = f'docker rm {container_name}'\n",
|
||||
"display(HTML(\"Use this link to: <a href=\\\"command:workbench.action.terminal.focus\\\">open the terminal window in Azure Data Studio</a> and use the links below to paste the command to the terminal.\"))\n",
|
||||
"display(HTML(\"Stop the container: <a href=\\\"command:workbench.action.terminal.sendSequence?%7B%22text%22%3A%22\"+stop_container_command.replace(\" \",\"%20\")+\"%22%7D\\\">\" + stop_container_command + \"</a>\"))\n",
|
||||
"display(HTML(\"Remove the container: <a href=\\\"command:workbench.action.terminal.sendSequence?%7B%22text%22%3A%22\"+remove_container_command.replace(\" \",\"%20\")+\"%22%7D\\\">\" + remove_container_command + \"</a>\"))"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3.10.1 64-bit",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.1"
|
||||
},
|
||||
"vscode": {
|
||||
"interpreter": {
|
||||
"hash": "878db934b706db2770cee331c11f15a67312cefb4f2334de757c7c9b6e34ef9f"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@@ -21,12 +21,12 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/Microsoft/azuredatastudio.git"
|
||||
},
|
||||
"capabilities": {
|
||||
"capabilities": {
|
||||
"virtualWorkspaces": false,
|
||||
"untrustedWorkspaces": {
|
||||
"supported": true
|
||||
}
|
||||
},
|
||||
},
|
||||
"extensionDependencies": [
|
||||
"microsoft.mssql",
|
||||
"microsoft.notebook"
|
||||
@@ -86,6 +86,10 @@
|
||||
"name": "version",
|
||||
"displayName": "%version-display-name%",
|
||||
"values": [
|
||||
{
|
||||
"name": "sql2022",
|
||||
"displayName": "%sql-2022-display-name%"
|
||||
},
|
||||
{
|
||||
"name": "sql2019",
|
||||
"displayName": "%sql-2019-display-name%"
|
||||
@@ -201,6 +205,58 @@
|
||||
}
|
||||
],
|
||||
"when": "version=sql2019"
|
||||
},
|
||||
{
|
||||
"name": "sql-image_2022",
|
||||
"dialog": {
|
||||
"notebook": "./notebooks/docker/2022/deploy-sql2022-image.ipynb",
|
||||
"title": "%docker-sql-2022-title%",
|
||||
"name": "docker-sql-2022-dialog",
|
||||
"tabs": [
|
||||
{
|
||||
"title": "",
|
||||
"sections": [
|
||||
{
|
||||
"title": "",
|
||||
"fields": [
|
||||
{
|
||||
"label": "%docker-container-name-field%",
|
||||
"variableName": "AZDATA_NB_VAR_DOCKER_CONTAINER_NAME",
|
||||
"type": "datetime_text",
|
||||
"defaultValue": "SQL2022-",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"label": "%docker-sql-password-field%",
|
||||
"variableName": "AZDATA_NB_VAR_DOCKER_PASSWORD",
|
||||
"type": "sql_password",
|
||||
"userName": "sa",
|
||||
"confirmationRequired": true,
|
||||
"confirmationLabel": "%docker-confirm-sql-password-field%",
|
||||
"defaultValue": "",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"label": "%docker-sql-port-field%",
|
||||
"variableName": "AZDATA_NB_VAR_DOCKER_PORT",
|
||||
"type": "number",
|
||||
"defaultValue": "1433",
|
||||
"required": true,
|
||||
"min": 1,
|
||||
"max": 65535
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"requiredTools": [
|
||||
{
|
||||
"name": "docker"
|
||||
}
|
||||
],
|
||||
"when": "version=sql2022"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -225,6 +281,10 @@
|
||||
"name": "version",
|
||||
"displayName": "%version-display-name%",
|
||||
"values": [
|
||||
{
|
||||
"name": "sql2022",
|
||||
"displayName": "%sql-2022-display-name%"
|
||||
},
|
||||
{
|
||||
"name": "sql2019",
|
||||
"displayName": "%sql-2019-display-name%"
|
||||
@@ -248,6 +308,12 @@
|
||||
"downloadUrl": "https://go.microsoft.com/fwlink/?linkid=866662",
|
||||
"requiredTools": [],
|
||||
"when": "version=sql2019"
|
||||
},
|
||||
{
|
||||
"name": "sql-windows-setup_2022",
|
||||
"webPageUrl": "https://go.microsoft.com/fwlink/?linkid=2195585",
|
||||
"requiredTools": [],
|
||||
"when": "version=sql2022"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
"version-display-name": "Version",
|
||||
"sql-2017-display-name": "SQL Server 2017",
|
||||
"sql-2019-display-name": "SQL Server 2019",
|
||||
"sql-2022-display-name": "SQL Server 2022 Preview",
|
||||
"docker-sql-2017-title": "Deploy SQL Server 2017 container images",
|
||||
"docker-sql-2019-title": "Deploy SQL Server 2019 container images",
|
||||
"docker-sql-2022-title": "Deploy SQL Server 2022 Preview container images",
|
||||
"docker-container-name-field": "Container name",
|
||||
"docker-sql-password-field": "SQL Server password",
|
||||
"docker-confirm-sql-password-field": "Confirm password",
|
||||
|
||||
@@ -244,9 +244,9 @@ export class ServiceSettingsPage extends ResourceTypePage {
|
||||
description: localize('deployCluster.AdvancedStorageDescription', "By default Controller storage settings will be applied to other services as well, you can expand the advanced storage settings to configure storage for other services.")
|
||||
});
|
||||
const controllerDataStorageClassInputInfo = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.controllerDataStorageClass', "Controller's data storage class"), width: inputWidth, required: true });
|
||||
const controllerDataStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.controllerDataStorageClaimSize', "Controller's data storage claim size"), width: inputWidth, required: true, min: 1 });
|
||||
const controllerDataStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.controllerDataStorageClaimSize', "Controller's data storage claim size (Gigabytes)"), width: inputWidth, required: true, min: 1 });
|
||||
const controllerLogsStorageClassInputInfo = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.controllerLogsStorageClass', "Controller's logs storage class"), width: inputWidth, required: true });
|
||||
const controllerLogsStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.controllerLogsStorageClaimSize', "Controller's logs storage claim size"), width: inputWidth, required: true, min: 1 });
|
||||
const controllerLogsStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.controllerLogsStorageClaimSize', "Controller's logs storage claim size (Gigabytes)"), width: inputWidth, required: true, min: 1 });
|
||||
|
||||
const storagePoolLabel = createLabel(view,
|
||||
{
|
||||
@@ -256,9 +256,9 @@ export class ServiceSettingsPage extends ResourceTypePage {
|
||||
});
|
||||
|
||||
const storagePoolDataStorageClassInputInfo = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.storagePoolDataStorageClass', "Storage pool's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const storagePoolDataStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.storagePoolDataStorageClaimSize', "Storage pool's data storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
const storagePoolDataStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.storagePoolDataStorageClaimSize', "Storage pool's data storage claim size (Gigabytes)"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
const storagePoolLogsStorageClassInputInfo = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.storagePoolLogsStorageClass', "Storage pool's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const storagePoolLogsStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.storagePoolLogsStorageClaimSize', "Storage pool's logs storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
const storagePoolLogsStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.storagePoolLogsStorageClaimSize', "Storage pool's logs storage claim size (Gigabytes)"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
|
||||
const dataPoolLabel = createLabel(view,
|
||||
{
|
||||
@@ -267,9 +267,9 @@ export class ServiceSettingsPage extends ResourceTypePage {
|
||||
required: false
|
||||
});
|
||||
const dataPoolDataStorageClassInputInfo = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.dataPoolDataStorageClass', "Data pool's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const dataPoolDataStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.dataPoolDataStorageClaimSize', "Data pool's data storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
const dataPoolDataStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.dataPoolDataStorageClaimSize', "Data pool's data storage claim size (Gigabytes)"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
const dataPoolLogsStorageClassInputInfo = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.dataPoolLogsStorageClass', "Data pool's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const dataPoolLogsStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.dataPoolLogsStorageClaimSize', "Data pool's logs storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
const dataPoolLogsStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.dataPoolLogsStorageClaimSize', "Data pool's logs storage claim size (Gigabytes)"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
|
||||
|
||||
const sqlServerMasterLabel = createLabel(view,
|
||||
@@ -280,9 +280,9 @@ export class ServiceSettingsPage extends ResourceTypePage {
|
||||
});
|
||||
|
||||
const sqlServerMasterDataStorageClassInputInfo = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.sqlServerMasterDataStorageClass', "SQL Server master's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const sqlServerMasterDataStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.sqlServerMasterDataStorageClaimSize', "SQL Server master's data storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
const sqlServerMasterDataStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.sqlServerMasterDataStorageClaimSize', "SQL Server master's data storage claim size (Gigabytes)"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
const sqlServerMasterLogsStorageClassInputInfo = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.sqlServerMasterLogsStorageClass', "SQL Server master's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields });
|
||||
const sqlServerMasterLogsStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.sqlServerMasterLogsStorageClaimSize', "SQL Server master's logs storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
const sqlServerMasterLogsStorageClaimSizeInputInfo = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.sqlServerMasterLogsStorageClaimSize', "SQL Server master's logs storage claim size (Gigabytes)"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields });
|
||||
|
||||
this.onNewInputComponentCreated(VariableNames.ControllerDataStorageClassName_VariableName, controllerDataStorageClassInputInfo);
|
||||
this.onNewInputComponentCreated(VariableNames.ControllerDataStorageSize_VariableName, controllerDataStorageClaimSizeInputInfo);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "schema-compare",
|
||||
"displayName": "%displayName%",
|
||||
"description": "%description%",
|
||||
"version": "1.15.0",
|
||||
"version": "1.16.0",
|
||||
"publisher": "Microsoft",
|
||||
"preview": false,
|
||||
"engines": {
|
||||
|
||||
@@ -63,7 +63,7 @@ export class SchemaCompareOptionsDialog {
|
||||
protected execute(): void {
|
||||
// Update the model deploymentoptions with the updated table component values
|
||||
this.optionsModel.setDeploymentOptions();
|
||||
this.optionsModel.setObjectTypeOptions();
|
||||
this.optionsModel.setIncludeObjectTypesToDeploymentOptions();
|
||||
// Set the publish deploymentoptions with the updated table component values
|
||||
this.schemaComparison.setDeploymentOptions(this.optionsModel.deploymentOptions);
|
||||
|
||||
@@ -102,6 +102,7 @@ export class SchemaCompareOptionsDialog {
|
||||
|
||||
// reset optionsvalueNameLookup with fresh deployment options
|
||||
this.optionsModel.setOptionsToValueNameLookup();
|
||||
this.optionsModel.setIncludeObjectTypesLookup();
|
||||
|
||||
await this.updateOptionsTable();
|
||||
this.optionsFlexBuilder.removeItem(this.optionsTable);
|
||||
@@ -182,11 +183,13 @@ export class SchemaCompareOptionsDialog {
|
||||
this.objectsTable = view.modelBuilder.table().component();
|
||||
await this.updateObjectsTable();
|
||||
|
||||
// Update inlcude object type options value on checkbox onchange
|
||||
this.disposableListeners.push(this.objectsTable.onCellAction((rowState) => {
|
||||
let checkboxState = <azdata.ICheckboxCellActionEventArgs>rowState;
|
||||
if (checkboxState && checkboxState.row !== undefined) {
|
||||
let label = this.optionsModel.objectTypeLabels[checkboxState.row];
|
||||
this.optionsModel.objectsLookup[label] = checkboxState.checked;
|
||||
// data[row][1] contains the include object type option display name
|
||||
const displayName = this.objectsTable?.data[checkboxState.row][1];
|
||||
this.optionsModel.setIncludeObjectTypesOptionValue(displayName, checkboxState.checked);
|
||||
this.optionsChanged = true;
|
||||
}
|
||||
}));
|
||||
@@ -229,7 +232,7 @@ export class SchemaCompareOptionsDialog {
|
||||
}
|
||||
|
||||
private async updateObjectsTable(): Promise<void> {
|
||||
let data = this.optionsModel.getObjectsData();
|
||||
let data = this.optionsModel.getIncludeObjectTypesOptionsData();
|
||||
await this.objectsTable.updateProperties({
|
||||
data: data,
|
||||
columns: [
|
||||
|
||||
@@ -91,77 +91,6 @@ export const save: string = localize('schemaCompare.saveFile', "Save");
|
||||
export function getConnectionString(caller: string): string { return localize('schemaCompare.GetConnectionString', "Do you want to connect to {0}?", caller); }
|
||||
export const selectConnection: string = localize('schemaCompare.selectConnection', "Select connection");
|
||||
|
||||
// object types
|
||||
export const Aggregates: string = localize('SchemaCompare.Aggregates', "Aggregates");
|
||||
export const ApplicationRoles: string = localize('SchemaCompare.ApplicationRoles', "Application Roles");
|
||||
export const Assemblies: string = localize('SchemaCompare.Assemblies', "Assemblies");
|
||||
export const AssemblyFiles: string = localize('SchemaCompare.AssemblyFiles', "Assembly Files");
|
||||
export const AsymmetricKeys: string = localize('SchemaCompare.AsymmetricKeys', "Asymmetric Keys");
|
||||
export const BrokerPriorities: string = localize('SchemaCompare.BrokerPriorities', "Broker Priorities");
|
||||
export const Certificates: string = localize('SchemaCompare.Certificates', "Certificates");
|
||||
export const ColumnEncryptionKeys: string = localize('SchemaCompare.ColumnEncryptionKeys', "Column Encryption Keys");
|
||||
export const ColumnMasterKeys: string = localize('SchemaCompare.ColumnMasterKeys', "Column Master Keys");
|
||||
export const Contracts: string = localize('SchemaCompare.Contracts', "Contracts");
|
||||
export const DatabaseOptions: string = localize('SchemaCompare.DatabaseOptions', "Database Options");
|
||||
export const DatabaseRoles: string = localize('SchemaCompare.DatabaseRoles', "Database Roles");
|
||||
export const DatabaseTriggers: string = localize('SchemaCompare.DatabaseTriggers', "Database Triggers");
|
||||
export const Defaults: string = localize('SchemaCompare.Defaults', "Defaults");
|
||||
export const ExtendedProperties: string = localize('SchemaCompare.ExtendedProperties', "Extended Properties");
|
||||
export const ExternalDataSources: string = localize('SchemaCompare.ExternalDataSources', "External Data Sources");
|
||||
export const ExternalFileFormats: string = localize('SchemaCompare.ExternalFileFormats', "External File Formats");
|
||||
export const ExternalStreams: string = localize('SchemaCompare.ExternalStreams', "External Streams");
|
||||
export const ExternalStreamingJobs: string = localize('SchemaCompare.ExternalStreamingJobs', "External Streaming Jobs");
|
||||
export const ExternalTables: string = localize('SchemaCompare.ExternalTables', "External Tables");
|
||||
export const Filegroups: string = localize('SchemaCompare.Filegroups', "Filegroups");
|
||||
export const Files: string = localize('SchemaCompare.Files', "Files");
|
||||
export const FileTables: string = localize('SchemaCompare.FileTables', "File Tables");
|
||||
export const FullTextCatalogs: string = localize('SchemaCompare.FullTextCatalogs', "Full Text Catalogs");
|
||||
export const FullTextStoplists: string = localize('SchemaCompare.FullTextStoplists', "Full Text Stoplists");
|
||||
export const MessageTypes: string = localize('SchemaCompare.MessageTypes', "Message Types");
|
||||
export const PartitionFunctions: string = localize('SchemaCompare.PartitionFunctions', "Partition Functions");
|
||||
export const PartitionSchemes: string = localize('SchemaCompare.PartitionSchemes', "Partition Schemes");
|
||||
export const Permissions: string = localize('SchemaCompare.Permissions', "Permissions");
|
||||
export const Queues: string = localize('SchemaCompare.Queues', "Queues");
|
||||
export const RemoteServiceBindings: string = localize('SchemaCompare.RemoteServiceBindings', "Remote Service Bindings");
|
||||
export const RoleMembership: string = localize('SchemaCompare.RoleMembership', "Role Membership");
|
||||
export const Rules: string = localize('SchemaCompare.Rules', "Rules");
|
||||
export const ScalarValuedFunctions: string = localize('SchemaCompare.ScalarValuedFunctions', "Scalar Valued Functions");
|
||||
export const SearchPropertyLists: string = localize('SchemaCompare.SearchPropertyLists', "Search Property Lists");
|
||||
export const SecurityPolicies: string = localize('SchemaCompare.SecurityPolicies', "Security Policies");
|
||||
export const Sequences: string = localize('SchemaCompare.Sequences', "Sequences");
|
||||
export const Services: string = localize('SchemaCompare.Services', "Services");
|
||||
export const Signatures: string = localize('SchemaCompare.Signatures', "Signatures");
|
||||
export const StoredProcedures: string = localize('SchemaCompare.StoredProcedures', "Stored Procedures");
|
||||
export const SymmetricKeys: string = localize('SchemaCompare.SymmetricKeys', "Symmetric Keys");
|
||||
export const Synonyms: string = localize('SchemaCompare.Synonyms', "Synonyms");
|
||||
export const Tables: string = localize('SchemaCompare.Tables', "Tables");
|
||||
export const TableValuedFunctions: string = localize('SchemaCompare.TableValuedFunctions', "Table Valued Functions");
|
||||
export const UserDefinedDataTypes: string = localize('SchemaCompare.UserDefinedDataTypes', "User Defined Data Types");
|
||||
export const UserDefinedTableTypes: string = localize('SchemaCompare.UserDefinedTableTypes', "User Defined Table Types");
|
||||
export const ClrUserDefinedTypes: string = localize('SchemaCompare.ClrUserDefinedTypes', "Clr User Defined Types");
|
||||
export const Users: string = localize('SchemaCompare.Users', "Users");
|
||||
export const Views: string = localize('SchemaCompare.Views', "Views");
|
||||
export const XmlSchemaCollections: string = localize('SchemaCompare.XmlSchemaCollections', "Xml Schema Collections");
|
||||
export const Audits: string = localize('SchemaCompare.Audits', "Audits");
|
||||
export const Credentials: string = localize('SchemaCompare.Credentials', "Credentials");
|
||||
export const CryptographicProviders: string = localize('SchemaCompare.CryptographicProviders', "Cryptographic Providers");
|
||||
export const DatabaseAuditSpecifications: string = localize('SchemaCompare.DatabaseAuditSpecifications', "Database Audit Specifications");
|
||||
export const DatabaseEncryptionKeys: string = localize('SchemaCompare.DatabaseEncryptionKeys', "Database Encryption Keys");
|
||||
export const DatabaseScopedCredentials: string = localize('SchemaCompare.DatabaseScopedCredentials', "Database Scoped Credentials");
|
||||
export const Endpoints: string = localize('SchemaCompare.Endpoints', "Endpoints");
|
||||
export const ErrorMessages: string = localize('SchemaCompare.ErrorMessages', "Error Messages");
|
||||
export const EventNotifications: string = localize('SchemaCompare.EventNotifications', "Event Notifications");
|
||||
export const EventSessions: string = localize('SchemaCompare.EventSessions', "Event Sessions");
|
||||
export const LinkedServerLogins: string = localize('SchemaCompare.LinkedServerLogins', "Linked Server Logins");
|
||||
export const LinkedServers: string = localize('SchemaCompare.LinkedServers', "Linked Servers");
|
||||
export const Logins: string = localize('SchemaCompare.Logins', "Logins");
|
||||
export const MasterKeys: string = localize('SchemaCompare.MasterKeys', "Master Keys");
|
||||
export const Routes: string = localize('SchemaCompare.Routes', "Routes");
|
||||
export const ServerAuditSpecifications: string = localize('SchemaCompare.ServerAuditSpecifications', "Server Audit Specifications");
|
||||
export const ServerRoleMembership: string = localize('SchemaCompare.ServerRoleMembership', "Server Role Membership");
|
||||
export const ServerRoles: string = localize('SchemaCompare.ServerRoles', "Server Roles");
|
||||
export const ServerTriggers: string = localize('SchemaCompare.ServerTriggers', "Server Triggers");
|
||||
|
||||
// Error messages
|
||||
export function compareErrorMessage(errorMessage: string): string { return localize('schemaCompare.compareErrorMessage', "Schema Compare failed: {0}", errorMessage ? errorMessage : 'Unknown'); }
|
||||
export function saveScmpErrorMessage(errorMessage: string): string { return localize('schemaCompare.saveScmpErrorMessage', "Save scmp failed: '{0}'", (errorMessage) ? errorMessage : 'Unknown'); }
|
||||
|
||||
@@ -5,16 +5,15 @@
|
||||
import * as loc from '../localizedConstants';
|
||||
import * as mssql from 'mssql';
|
||||
import * as vscode from 'vscode';
|
||||
import { isNullOrUndefined } from 'util';
|
||||
|
||||
export class SchemaCompareOptionsModel {
|
||||
// key is the option display name and values are checkboxValue and optionName
|
||||
private optionsValueNameLookup: { [key: string]: mssql.IOptionWithValue } = {};
|
||||
public excludedObjectTypes: number[] = [];
|
||||
public objectsLookup = {};
|
||||
private includeObjectTypesLookup: { [key: string]: mssql.IOptionWithValue } = {};
|
||||
|
||||
constructor(public deploymentOptions: mssql.DeploymentOptions) {
|
||||
this.setOptionsToValueNameLookup();
|
||||
this.setIncludeObjectTypesLookup();
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -72,590 +71,62 @@ export class SchemaCompareOptionsModel {
|
||||
return optionName !== undefined ? this.deploymentOptions.booleanOptionsDictionary[optionName.optionName].description : '';
|
||||
}
|
||||
|
||||
//#region Schema Compare Objects
|
||||
public objectTypeLabels: string[] = [
|
||||
loc.Aggregates,
|
||||
loc.ApplicationRoles,
|
||||
loc.Assemblies,
|
||||
loc.AssemblyFiles,
|
||||
loc.AsymmetricKeys,
|
||||
loc.BrokerPriorities,
|
||||
loc.Certificates,
|
||||
loc.ColumnEncryptionKeys,
|
||||
loc.ColumnMasterKeys,
|
||||
loc.Contracts,
|
||||
loc.DatabaseOptions,
|
||||
loc.DatabaseRoles,
|
||||
loc.DatabaseTriggers,
|
||||
loc.Defaults,
|
||||
loc.ExtendedProperties,
|
||||
loc.ExternalDataSources,
|
||||
loc.ExternalFileFormats,
|
||||
loc.ExternalStreams,
|
||||
loc.ExternalStreamingJobs,
|
||||
loc.ExternalTables,
|
||||
loc.Filegroups,
|
||||
loc.Files,
|
||||
loc.FileTables,
|
||||
loc.FullTextCatalogs,
|
||||
loc.FullTextStoplists,
|
||||
loc.MessageTypes,
|
||||
loc.PartitionFunctions,
|
||||
loc.PartitionSchemes,
|
||||
loc.Permissions,
|
||||
loc.Queues,
|
||||
loc.RemoteServiceBindings,
|
||||
loc.RoleMembership,
|
||||
loc.Rules,
|
||||
loc.ScalarValuedFunctions,
|
||||
loc.SearchPropertyLists,
|
||||
loc.SecurityPolicies,
|
||||
loc.Sequences,
|
||||
loc.Services,
|
||||
loc.Signatures,
|
||||
loc.StoredProcedures,
|
||||
loc.SymmetricKeys,
|
||||
loc.Synonyms,
|
||||
loc.Tables,
|
||||
loc.TableValuedFunctions,
|
||||
loc.UserDefinedDataTypes,
|
||||
loc.UserDefinedTableTypes,
|
||||
loc.ClrUserDefinedTypes,
|
||||
loc.Users,
|
||||
loc.Views,
|
||||
loc.XmlSchemaCollections,
|
||||
loc.Audits,
|
||||
loc.Credentials,
|
||||
loc.CryptographicProviders,
|
||||
loc.DatabaseAuditSpecifications,
|
||||
loc.DatabaseEncryptionKeys,
|
||||
loc.DatabaseScopedCredentials,
|
||||
loc.Endpoints,
|
||||
loc.ErrorMessages,
|
||||
loc.EventNotifications,
|
||||
loc.EventSessions,
|
||||
loc.LinkedServerLogins,
|
||||
loc.LinkedServers,
|
||||
loc.Logins,
|
||||
loc.MasterKeys,
|
||||
loc.Routes,
|
||||
loc.ServerAuditSpecifications,
|
||||
loc.ServerRoleMembership,
|
||||
loc.ServerRoles,
|
||||
loc.ServerTriggers
|
||||
].sort();
|
||||
|
||||
public getObjectsData(): string[][] {
|
||||
let data = [];
|
||||
this.objectsLookup = {};
|
||||
this.objectTypeLabels.forEach(l => {
|
||||
let checked: boolean = this.getSchemaCompareIncludedObjectsUtil(l);
|
||||
data.push([checked, l]);
|
||||
this.objectsLookup[l] = checked;
|
||||
/*
|
||||
* Sets include object types option's checkbox values and property name to the includeObjectTypesLookup map
|
||||
*/
|
||||
public setIncludeObjectTypesLookup(): void {
|
||||
Object.entries(this.deploymentOptions.objectTypesDictionary).forEach(option => {
|
||||
const optionValue: mssql.IOptionWithValue = {
|
||||
optionName: option[0],
|
||||
checked: this.getIncludeObjectTypeOptionCheckStatus(option[0])
|
||||
};
|
||||
this.includeObjectTypesLookup[option[1]] = optionValue;
|
||||
});
|
||||
return data;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
||||
public setObjectTypeOptions() {
|
||||
for (let option in this.objectsLookup) {
|
||||
this.setSchemaCompareIncludedObjectsUtil(option, this.objectsLookup[option]);
|
||||
}
|
||||
this.deploymentOptions.excludeObjectTypes.value = this.excludedObjectTypes;
|
||||
}
|
||||
|
||||
public getSchemaCompareIncludedObjectsUtil(label): boolean {
|
||||
switch (label) {
|
||||
case loc.Aggregates:
|
||||
return !isNullOrUndefined(this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Aggregates)) ? false : true;
|
||||
case loc.ApplicationRoles:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ApplicationRoles)) ? false : true;
|
||||
case loc.Assemblies:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Assemblies)) ? false : true;
|
||||
case loc.AssemblyFiles:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.AssemblyFiles)) ? false : true;
|
||||
case loc.AsymmetricKeys:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.AsymmetricKeys)) ? false : true;
|
||||
case loc.BrokerPriorities:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.BrokerPriorities)) ? false : true;
|
||||
case loc.Certificates:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Certificates)) ? false : true;
|
||||
case loc.ColumnEncryptionKeys:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ColumnEncryptionKeys)) ? false : true;
|
||||
case loc.ColumnMasterKeys:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ColumnMasterKeys)) ? false : true;
|
||||
case loc.Contracts:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Contracts)) ? false : true;
|
||||
case loc.DatabaseOptions:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.DatabaseOptions)) ? false : true;
|
||||
case loc.DatabaseRoles:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.DatabaseRoles)) ? false : true;
|
||||
case loc.DatabaseTriggers:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.DatabaseTriggers)) ? false : true;
|
||||
case loc.Defaults:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Defaults)) ? false : true;
|
||||
case loc.ExtendedProperties:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ExtendedProperties)) ? false : true;
|
||||
case loc.ExternalDataSources:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ExternalDataSources)) ? false : true;
|
||||
case loc.ExternalFileFormats:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ExternalFileFormats)) ? false : true;
|
||||
case loc.ExternalStreams:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ExternalStreams)) ? false : true;
|
||||
case loc.ExternalStreamingJobs:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ExternalStreamingJobs)) ? false : true;
|
||||
case loc.ExternalTables:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ExternalTables)) ? false : true;
|
||||
case loc.Filegroups:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Filegroups)) ? false : true;
|
||||
case loc.Files:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Files)) ? false : true;
|
||||
case loc.FileTables:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.FileTables)) ? false : true;
|
||||
case loc.FullTextCatalogs:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.FullTextCatalogs)) ? false : true;
|
||||
case loc.FullTextStoplists:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.FullTextStoplists)) ? false : true;
|
||||
case loc.MessageTypes:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.MessageTypes)) ? false : true;
|
||||
case loc.PartitionFunctions:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.PartitionFunctions)) ? false : true;
|
||||
case loc.PartitionSchemes:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.PartitionSchemes)) ? false : true;
|
||||
case loc.Permissions:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Permissions)) ? false : true;
|
||||
case loc.Queues:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Queues)) ? false : true;
|
||||
case loc.RemoteServiceBindings:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.RemoteServiceBindings)) ? false : true;
|
||||
case loc.RoleMembership:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.RoleMembership)) ? false : true;
|
||||
case loc.Rules:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Rules)) ? false : true;
|
||||
case loc.ScalarValuedFunctions:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ScalarValuedFunctions)) ? false : true;
|
||||
case loc.SearchPropertyLists:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.SearchPropertyLists)) ? false : true;
|
||||
case loc.SecurityPolicies:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.SecurityPolicies)) ? false : true;
|
||||
case loc.Sequences:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Sequences)) ? false : true;
|
||||
case loc.Services:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Services)) ? false : true;
|
||||
case loc.Signatures:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Signatures)) ? false : true;
|
||||
case loc.StoredProcedures:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.StoredProcedures)) ? false : true;
|
||||
case loc.SymmetricKeys:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.SymmetricKeys)) ? false : true;
|
||||
case loc.Synonyms:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Synonyms)) ? false : true;
|
||||
case loc.Tables:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Tables)) ? false : true;
|
||||
case loc.TableValuedFunctions:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.TableValuedFunctions)) ? false : true;
|
||||
case loc.UserDefinedDataTypes:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.UserDefinedDataTypes)) ? false : true;
|
||||
case loc.UserDefinedTableTypes:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.UserDefinedTableTypes)) ? false : true;
|
||||
case loc.ClrUserDefinedTypes:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ClrUserDefinedTypes)) ? false : true;
|
||||
case loc.Users:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Users)) ? false : true;
|
||||
case loc.Views:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Views)) ? false : true;
|
||||
case loc.XmlSchemaCollections:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.XmlSchemaCollections)) ? false : true;
|
||||
case loc.Audits:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Audits)) ? false : true;
|
||||
case loc.Credentials:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Credentials)) ? false : true;
|
||||
case loc.CryptographicProviders:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.CryptographicProviders)) ? false : true;
|
||||
case loc.DatabaseAuditSpecifications:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.DatabaseAuditSpecifications)) ? false : true;
|
||||
case loc.DatabaseEncryptionKeys:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.DatabaseEncryptionKeys)) ? false : true;
|
||||
case loc.DatabaseScopedCredentials:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.DatabaseScopedCredentials)) ? false : true;
|
||||
case loc.Endpoints:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Endpoints)) ? false : true;
|
||||
case loc.ErrorMessages:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ErrorMessages)) ? false : true;
|
||||
case loc.EventNotifications:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.EventNotifications)) ? false : true;
|
||||
case loc.EventSessions:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.EventSessions)) ? false : true;
|
||||
case loc.LinkedServerLogins:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.LinkedServerLogins)) ? false : true;
|
||||
case loc.LinkedServers:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.LinkedServers)) ? false : true;
|
||||
case loc.Logins:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Logins)) ? false : true;
|
||||
case loc.MasterKeys:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.MasterKeys)) ? false : true;
|
||||
case loc.Routes:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.Routes)) ? false : true;
|
||||
case loc.ServerAuditSpecifications:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ServerAuditSpecifications)) ? false : true;
|
||||
case loc.ServerRoleMembership:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ServerRoleMembership)) ? false : true;
|
||||
case loc.ServerRoles:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ServerRoles)) ? false : true;
|
||||
case loc.ServerTriggers:
|
||||
return (this.deploymentOptions.excludeObjectTypes.value.find(x => x === mssql.SchemaObjectType.ServerTriggers)) ? false : true;
|
||||
}
|
||||
return false;
|
||||
/*
|
||||
* Initialize options data from include objects options for table component
|
||||
* Returns data as [booleanValue, optionName]
|
||||
*/
|
||||
public getIncludeObjectTypesOptionsData(): any[][] {
|
||||
let data: any[][] = [];
|
||||
Object.entries(this.deploymentOptions.objectTypesDictionary).forEach(option => {
|
||||
// option[1] is the display name and option[0] is the optionName
|
||||
data.push([this.getIncludeObjectTypeOptionCheckStatus(option[0]), option[1]]);
|
||||
});
|
||||
|
||||
return data.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
}
|
||||
|
||||
public setSchemaCompareIncludedObjectsUtil(label: string, included: boolean) {
|
||||
switch (label) {
|
||||
case loc.Aggregates:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Aggregates);
|
||||
}
|
||||
return;
|
||||
case loc.ApplicationRoles:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.ApplicationRoles);
|
||||
}
|
||||
return;
|
||||
case loc.Assemblies:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Assemblies);
|
||||
}
|
||||
return;
|
||||
case loc.AssemblyFiles:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.AssemblyFiles);
|
||||
}
|
||||
return;
|
||||
case loc.AsymmetricKeys:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.AsymmetricKeys);
|
||||
}
|
||||
return;
|
||||
case loc.BrokerPriorities:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.BrokerPriorities);
|
||||
}
|
||||
return;
|
||||
case loc.Certificates:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Certificates);
|
||||
}
|
||||
return;
|
||||
case loc.ColumnEncryptionKeys:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.ColumnEncryptionKeys);
|
||||
}
|
||||
return;
|
||||
case loc.ColumnMasterKeys:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.ColumnMasterKeys);
|
||||
}
|
||||
return;
|
||||
case loc.Contracts:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Contracts);
|
||||
}
|
||||
return;
|
||||
case loc.DatabaseOptions:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.DatabaseOptions);
|
||||
}
|
||||
return;
|
||||
case loc.DatabaseRoles:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.DatabaseRoles);
|
||||
}
|
||||
return;
|
||||
case loc.DatabaseTriggers:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.DatabaseTriggers);
|
||||
}
|
||||
return;
|
||||
case loc.Defaults:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Defaults);
|
||||
}
|
||||
return;
|
||||
case loc.ExtendedProperties:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.ExtendedProperties);
|
||||
}
|
||||
return;
|
||||
case loc.ExternalDataSources:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.ExternalDataSources);
|
||||
}
|
||||
return;
|
||||
case loc.ExternalFileFormats:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.ExternalFileFormats);
|
||||
}
|
||||
return;
|
||||
case loc.ExternalStreams:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.ExternalStreams);
|
||||
}
|
||||
return;
|
||||
case loc.ExternalStreamingJobs:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.ExternalStreamingJobs);
|
||||
}
|
||||
return;
|
||||
case loc.ExternalTables:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.ExternalTables);
|
||||
}
|
||||
return;
|
||||
case loc.Filegroups:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Filegroups);
|
||||
}
|
||||
return;
|
||||
case loc.Files:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Files);
|
||||
}
|
||||
return;
|
||||
case loc.FileTables:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.FileTables);
|
||||
}
|
||||
return;
|
||||
case loc.FullTextCatalogs:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.FullTextCatalogs);
|
||||
}
|
||||
return;
|
||||
case loc.FullTextStoplists:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.FullTextStoplists);
|
||||
}
|
||||
return;
|
||||
case loc.MessageTypes:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.MessageTypes);
|
||||
}
|
||||
return;
|
||||
case loc.PartitionFunctions:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.PartitionFunctions);
|
||||
}
|
||||
return;
|
||||
case loc.PartitionSchemes:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.PartitionSchemes);
|
||||
}
|
||||
return;
|
||||
case loc.Permissions:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Permissions);
|
||||
}
|
||||
return;
|
||||
case loc.Queues:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Queues);
|
||||
}
|
||||
return;
|
||||
case loc.RemoteServiceBindings:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.RemoteServiceBindings);
|
||||
}
|
||||
return;
|
||||
case loc.RoleMembership:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.RoleMembership);
|
||||
}
|
||||
return;
|
||||
case loc.Rules:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Rules);
|
||||
}
|
||||
return;
|
||||
case loc.ScalarValuedFunctions:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.ScalarValuedFunctions);
|
||||
}
|
||||
return;
|
||||
case loc.SearchPropertyLists:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.SearchPropertyLists);
|
||||
}
|
||||
return;
|
||||
case loc.SecurityPolicies:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.SecurityPolicies);
|
||||
}
|
||||
return;
|
||||
case loc.Sequences:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Sequences);
|
||||
}
|
||||
return;
|
||||
case loc.Services:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Services);
|
||||
}
|
||||
return;
|
||||
case loc.Signatures:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Signatures);
|
||||
}
|
||||
return;
|
||||
case loc.StoredProcedures:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.StoredProcedures);
|
||||
}
|
||||
return;
|
||||
case loc.SymmetricKeys:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.SymmetricKeys);
|
||||
}
|
||||
return;
|
||||
case loc.Synonyms:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Synonyms);
|
||||
}
|
||||
return;
|
||||
case loc.Tables:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Tables);
|
||||
}
|
||||
return;
|
||||
case loc.TableValuedFunctions:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.TableValuedFunctions);
|
||||
}
|
||||
return;
|
||||
case loc.UserDefinedDataTypes:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.UserDefinedDataTypes);
|
||||
}
|
||||
return;
|
||||
case loc.UserDefinedTableTypes:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.UserDefinedTableTypes);
|
||||
}
|
||||
return;
|
||||
case loc.ClrUserDefinedTypes:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.ClrUserDefinedTypes);
|
||||
}
|
||||
return;
|
||||
case loc.Users:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Users);
|
||||
}
|
||||
return;
|
||||
case loc.Views:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Views);
|
||||
}
|
||||
return;
|
||||
case loc.XmlSchemaCollections:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.XmlSchemaCollections);
|
||||
}
|
||||
return;
|
||||
case loc.Audits:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Audits);
|
||||
}
|
||||
return;
|
||||
case loc.Credentials:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Credentials);
|
||||
}
|
||||
return;
|
||||
case loc.CryptographicProviders:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.CryptographicProviders);
|
||||
}
|
||||
return;
|
||||
case loc.DatabaseAuditSpecifications:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.DatabaseAuditSpecifications);
|
||||
}
|
||||
return;
|
||||
case loc.DatabaseEncryptionKeys:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.DatabaseEncryptionKeys);
|
||||
}
|
||||
return;
|
||||
case loc.DatabaseScopedCredentials:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.DatabaseScopedCredentials);
|
||||
}
|
||||
return;
|
||||
case loc.Endpoints:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Endpoints);
|
||||
}
|
||||
return;
|
||||
case loc.ErrorMessages:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.ErrorMessages);
|
||||
}
|
||||
return;
|
||||
case loc.EventNotifications:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.EventNotifications);
|
||||
}
|
||||
return;
|
||||
case loc.EventSessions:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.EventSessions);
|
||||
}
|
||||
return;
|
||||
case loc.LinkedServerLogins:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.LinkedServerLogins);
|
||||
}
|
||||
return;
|
||||
case loc.LinkedServers:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.LinkedServers);
|
||||
}
|
||||
return;
|
||||
case loc.Logins:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Logins);
|
||||
}
|
||||
return;
|
||||
case loc.MasterKeys:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.MasterKeys);
|
||||
}
|
||||
return;
|
||||
case loc.Routes:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.Routes);
|
||||
}
|
||||
return;
|
||||
case loc.ServerAuditSpecifications:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.ServerAuditSpecifications);
|
||||
}
|
||||
return;
|
||||
case loc.ServerRoleMembership:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.ServerRoleMembership);
|
||||
}
|
||||
return;
|
||||
case loc.ServerRoles:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.ServerRoles);
|
||||
}
|
||||
return;
|
||||
case loc.ServerTriggers:
|
||||
if (!included) {
|
||||
this.excludedObjectTypes.push(mssql.SchemaObjectType.ServerTriggers);
|
||||
}
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* Gets the selected/default value of the object type option
|
||||
* return false for the deploymentOptions.excludeObjectTypes option, if it is in ObjectTypesDictionary
|
||||
*/
|
||||
public getIncludeObjectTypeOptionCheckStatus(optionName: string): boolean {
|
||||
return (this.deploymentOptions.excludeObjectTypes.value?.find(x => x.toLowerCase() === optionName.toLowerCase())) !== undefined ? false : true;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Sets the checkbox value to the includeObjectTypesLookup map
|
||||
*/
|
||||
public setIncludeObjectTypesOptionValue(displayName: string, checked: boolean): void {
|
||||
this.includeObjectTypesLookup[displayName].checked = checked;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sets the selected option checkbox value to the deployment options
|
||||
*/
|
||||
public setIncludeObjectTypesToDeploymentOptions(): void {
|
||||
let finalExcludedObjectTypes: string[] = [];
|
||||
Object.entries(this.includeObjectTypesLookup).forEach(option => {
|
||||
// option[1] holds checkedbox value and optionName
|
||||
// sending the unchecked(false) options only to the excludeObjectTypes
|
||||
if (!option[1].checked) {
|
||||
finalExcludedObjectTypes.push(option[1].optionName);
|
||||
}
|
||||
});
|
||||
|
||||
this.deploymentOptions.excludeObjectTypes.value = finalExcludedObjectTypes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1126,6 +1126,20 @@ export class SchemaCompareMainWindow {
|
||||
if (ownerUri) {
|
||||
endpointInfo = endpoint;
|
||||
endpointInfo.ownerUri = ownerUri;
|
||||
} else if (endpoint.endpointType === mssql.SchemaCompareEndpointType.Project) {
|
||||
endpointInfo = {
|
||||
endpointType: endpoint.endpointType,
|
||||
packageFilePath: '',
|
||||
serverDisplayName: '',
|
||||
serverName: '',
|
||||
databaseName: '',
|
||||
ownerUri: '',
|
||||
connectionDetails: undefined,
|
||||
projectFilePath: endpoint.projectFilePath,
|
||||
targetScripts: [],
|
||||
dataSchemaProvider: '',
|
||||
folderStructure: loc.schemaObjectType // TODO: Pick this automatically from the scmp file, after issue #20332 is resolved (check dsp as well)
|
||||
};
|
||||
} else {
|
||||
// need to do this instead of just setting it to the endpoint because some fields are null which will cause an error when sending the compare request
|
||||
endpointInfo = {
|
||||
|
||||
@@ -11,30 +11,42 @@ describe('Schema Compare Options Model', () => {
|
||||
it('Should create model and set options successfully', function (): void {
|
||||
const model = new SchemaCompareOptionsModel(testUtils.getDeploymentOptions());
|
||||
should.notEqual(model.getOptionsData(), undefined, 'Options shouldn\'t be undefined');
|
||||
should.notEqual(model.getObjectsData(), undefined, 'Objects shouldn\'t be undefined');
|
||||
should.notEqual(model.getIncludeObjectTypesOptionsData(), undefined, 'Objects shouldn\'t be undefined');
|
||||
|
||||
should.doesNotThrow(() => model.setDeploymentOptions());
|
||||
should.doesNotThrow(() => model.setObjectTypeOptions());
|
||||
|
||||
should(model.getSchemaCompareIncludedObjectsUtil('')).be.false('Should return false if invalid object name is passed in');
|
||||
should.doesNotThrow(() => model.setIncludeObjectTypesToDeploymentOptions());
|
||||
});
|
||||
|
||||
it('Should exclude objects', function (): void {
|
||||
it('Should not have a default object types to exclude from IncludeObjectTypes ', function (): void {
|
||||
const model = new SchemaCompareOptionsModel(testUtils.getDeploymentOptions());
|
||||
should(model.excludedObjectTypes.length).be.equal(0, 'There should be no excluded objects');
|
||||
should(model.deploymentOptions.excludeObjectTypes.value.length).be.equal(0, 'There should be no object type excluded from IncludeObjectTypes');
|
||||
|
||||
model.objectTypeLabels.forEach(label => {
|
||||
model.setSchemaCompareIncludedObjectsUtil(label, false);
|
||||
Object.keys(model.deploymentOptions.objectTypesDictionary).forEach(option => {
|
||||
should(model.getIncludeObjectTypeOptionCheckStatus(option)).equal(true, 'Object types that are not excluded should return true');
|
||||
});
|
||||
});
|
||||
|
||||
should(model.excludedObjectTypes.length).be.equal(model.objectTypeLabels.length, 'All the object types should be excluded');
|
||||
it('Should have default object types to exclude from IncludeObjectTypes ', function (): void {
|
||||
const model = new SchemaCompareOptionsModel(testUtils.getDeploymentOptions());
|
||||
model.deploymentOptions.excludeObjectTypes.value = ['SampleProperty1'];
|
||||
|
||||
should(model.deploymentOptions.excludeObjectTypes.value.length).be.equal(1, 'There should be one object type excluding from IncludeObjectTypes ');
|
||||
|
||||
// should return false for the default object types and false for the remaining object types
|
||||
Object.keys(model.deploymentOptions.objectTypesDictionary).forEach(option => {
|
||||
if (option === 'SampleProperty1') {
|
||||
should(model.getIncludeObjectTypeOptionCheckStatus(option)).equal(false, 'Object type property that have default object types to exclude from IncludeObjectTypes should return false');
|
||||
} else {
|
||||
should(model.getIncludeObjectTypeOptionCheckStatus(option)).equal(true, 'All including Object type should return true');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('Should get descriptions', function (): void {
|
||||
const model = new SchemaCompareOptionsModel(testUtils.getDeploymentOptions());
|
||||
model.getOptionsData();
|
||||
Object.entries(model.deploymentOptions.booleanOptionsDictionary).forEach(option => {
|
||||
should(model.getOptionDescription(option[1].displayName)).not.equal(undefined);
|
||||
should(model.getOptionDescription(option[1].displayName)).not.equal(undefined, 'Option description shouldn\'t be undefined');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -117,6 +117,10 @@ export function getDeploymentOptions(): mssql.DeploymentOptions {
|
||||
booleanOptionsDictionary: {
|
||||
'SampleDisplayOption1': { value: false, description: sampleDesc, displayName: sampleName },
|
||||
'SampleDisplayOption2': { value: false, description: sampleDesc, displayName: sampleName }
|
||||
},
|
||||
objectTypesDictionary: {
|
||||
'SampleProperty1': sampleName,
|
||||
'SampleProperty2': sampleName
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ConnectionDetails, IConnectionInfo } from 'vscode-mssql';
|
||||
import { AzureFunctionsExtensionApi } from '../../../types/vscode-azurefunctions.api';
|
||||
// https://github.com/microsoft/vscode-azuretools/blob/main/ui/api.d.ts
|
||||
import { AzureExtensionApiProvider } from '../../../types/vscode-azuretools.api';
|
||||
import { TelemetryActions, TelemetryReporter, TelemetryViews } from './telemetry';
|
||||
/**
|
||||
* Represents the settings in an Azure function project's locawl.settings.json file
|
||||
*/
|
||||
@@ -24,10 +25,7 @@ export interface ILocalSettingsJson {
|
||||
ConnectionStrings?: { [key: string]: string };
|
||||
}
|
||||
|
||||
export interface IFileFunctionObject {
|
||||
filePromise: Promise<string>;
|
||||
watcherDisposable: vscode.Disposable;
|
||||
}
|
||||
export const outputChannel = vscode.window.createOutputChannel(constants.serviceName);
|
||||
|
||||
/**
|
||||
* copied and modified from vscode-azurefunctions extension
|
||||
@@ -198,47 +196,16 @@ export async function getSettingsFile(projectFolder: string): Promise<string | u
|
||||
}
|
||||
|
||||
/**
|
||||
* New azure function file watcher and watcher disposable to be used to watch for changes to the azure function project
|
||||
* @param projectFolder is the parent directory to the project file
|
||||
* @returns the function file path once created and the watcher disposable
|
||||
* Adds the latest SQL nuget package to the project
|
||||
* @param projectFolder is the folder containing the project file
|
||||
*/
|
||||
export function waitForNewFunctionFile(projectFolder: string): IFileFunctionObject {
|
||||
const watcher = vscode.workspace.createFileSystemWatcher((
|
||||
new vscode.RelativePattern(projectFolder, '**/*.cs')), false, true, true);
|
||||
const filePromise = new Promise<string>((resolve, _) => {
|
||||
watcher.onDidCreate((e) => {
|
||||
resolve(e.fsPath);
|
||||
});
|
||||
});
|
||||
return {
|
||||
filePromise,
|
||||
watcherDisposable: watcher
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the new host project file once it has created and the watcher disposable
|
||||
* @returns the host file path once created and the watcher disposable
|
||||
*/
|
||||
export function waitForNewHostFile(): IFileFunctionObject {
|
||||
const watcher = vscode.workspace.createFileSystemWatcher('**/host.json', false, true, true);
|
||||
const filePromise = new Promise<string>((resolve, _) => {
|
||||
watcher.onDidCreate((e) => {
|
||||
resolve(e.fsPath);
|
||||
});
|
||||
});
|
||||
return {
|
||||
filePromise,
|
||||
watcherDisposable: watcher
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the required nuget package to the project
|
||||
* @param selectedProjectFile is the users selected project file path
|
||||
*/
|
||||
export async function addSqlNugetReferenceToProjectFile(selectedProjectFile: string): Promise<void> {
|
||||
await utils.executeCommand(`dotnet add "${selectedProjectFile}" package ${constants.sqlExtensionPackageName} --prerelease`);
|
||||
export async function addSqlNugetReferenceToProjectFile(projectFolder: string): Promise<void> {
|
||||
// clear the output channel prior to adding the nuget reference
|
||||
outputChannel.clear();
|
||||
let addNugetCommmand = await utils.executeCommand(`dotnet add "${projectFolder}" package ${constants.sqlExtensionPackageName} --prerelease`);
|
||||
outputChannel.appendLine(constants.dotnetResult(addNugetCommmand));
|
||||
outputChannel.show(true);
|
||||
TelemetryReporter.sendActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.addSQLNugetPackage);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -510,8 +477,6 @@ export async function promptAndUpdateConnectionStringSetting(projectUri: vscode.
|
||||
connectionStringSettingName = selectedSetting?.label;
|
||||
}
|
||||
}
|
||||
// Add sql extension package reference to project. If the reference is already there, it doesn't get added again
|
||||
await addSqlNugetReferenceToProjectFile(projectUri.fsPath);
|
||||
} else {
|
||||
// if no AF project was found or there's more than one AF functions project in the workspace,
|
||||
// ask for the user to input the setting name
|
||||
@@ -617,19 +582,9 @@ export async function promptSelectDatabase(connectionURI: string): Promise<strin
|
||||
|
||||
export async function getConnectionURI(connectionInfo: IConnectionInfo): Promise<string | undefined> {
|
||||
const vscodeMssqlApi = await utils.getVscodeMssqlApi();
|
||||
|
||||
let connectionURI: string = '';
|
||||
try {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: constants.connectionProgressTitle,
|
||||
cancellable: false
|
||||
}, async (_progress, _token) => {
|
||||
// show progress bar while connecting to the users selected connection profile
|
||||
connectionURI = await vscodeMssqlApi.connect(connectionInfo!);
|
||||
}
|
||||
);
|
||||
connectionURI = await vscodeMssqlApi.connect(connectionInfo);
|
||||
} catch (e) {
|
||||
// mssql connection error will be shown to the user
|
||||
return undefined;
|
||||
|
||||
@@ -22,6 +22,7 @@ export const sqlBindingsHelpLink = 'https://github.com/Azure/azure-functions-sql
|
||||
export const passwordPlaceholder = '******';
|
||||
export const azureFunctionLocalSettingsFileName = 'local.settings.json';
|
||||
export const vscodeOpenCommand = 'vscode.open';
|
||||
export const serviceName = 'SQL Bindings';
|
||||
|
||||
// localized constants
|
||||
export const functionNameTitle = localize('functionNameTitle', 'Function Name');
|
||||
@@ -73,7 +74,6 @@ export const userPasswordLater = localize('userPasswordLater', 'In order to user
|
||||
export const openFile = localize('openFile', "Open File");
|
||||
export const closeButton = localize('closeButton', "Close");
|
||||
export const enterPasswordPrompt = localize('enterPasswordPrompt', '(Optional) Enter connection password to save in local.settings.json');
|
||||
export const connectionProgressTitle = localize('connectionProgressTitle', "Testing SQL Server connection...");
|
||||
export const enterTableName = localize('enterTableName', 'Enter SQL table to query');
|
||||
export const enterViewName = localize('enterViewName', 'Enter SQL view to query');
|
||||
export const enterTableNameToUpsert = localize('enterTableNameToUpsert', 'Enter SQL table to upsert into');
|
||||
@@ -91,6 +91,7 @@ export function failedToParse(filename: string, error: any): string { return loc
|
||||
export function addSqlBinding(functionName: string): string { return localize('addSqlBinding', 'Adding SQL Binding to function "{0}"...'), functionName; }
|
||||
export function errorNewAzureFunction(error: any): string { return localize('errorNewAzureFunction', 'Error creating new Azure Function: {0}', utils.getErrorMessage(error)); }
|
||||
export function manuallyEnterObjectName(userObjectName: string): string { return `$(pencil) ${userObjectName}`; }
|
||||
export function dotnetResult(output: string): string { return localize('dotnetResult', 'Adding SQL nuget package:\n{0}', output); }
|
||||
|
||||
// Known Azure settings reference for Azure Functions
|
||||
// https://docs.microsoft.com/en-us/azure/azure-functions/functions-app-settings
|
||||
|
||||
@@ -11,7 +11,8 @@ export const TelemetryReporter = new AdsTelemetryReporter(packageInfo.name, pack
|
||||
|
||||
export enum TelemetryViews {
|
||||
SqlBindingsQuickPick = 'SqlBindingsQuickPick',
|
||||
CreateAzureFunctionWithSqlBinding = 'CreateAzureFunctionWithSqlBinding'
|
||||
CreateAzureFunctionWithSqlBinding = 'CreateAzureFunctionWithSqlBinding',
|
||||
AzureFunctionsUtils = 'AzureFunctionsUtils',
|
||||
}
|
||||
|
||||
export enum TelemetryActions {
|
||||
@@ -28,9 +29,13 @@ export enum TelemetryActions {
|
||||
updateConnectionString = 'updateConnectionString',
|
||||
finishAddSqlBinding = 'finishAddSqlBinding',
|
||||
exitSqlBindingsQuickpick = 'exitSqlBindingsQuickpick',
|
||||
|
||||
// Azure Functions Utils
|
||||
addSQLNugetPackage = 'addSQLNugetPackage',
|
||||
}
|
||||
|
||||
export enum CreateAzureFunctionStep {
|
||||
noAzureFunctionsExtension = 'noAzureFunctionsExtension',
|
||||
getAzureFunctionProject = 'getAzureFunctionProject',
|
||||
learnMore = 'learnMore',
|
||||
helpCreateAzureFunctionProject = 'helpCreateAzureFunctionProject',
|
||||
|
||||
@@ -83,20 +83,6 @@ export function generateQuotedFullName(schema: string, objectName: string): stri
|
||||
return `[${escapeClosingBrackets(schema)}].[${escapeClosingBrackets(objectName)}]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that will reject after the specified timeout
|
||||
* @param errorMessage error message to be returned in the rejection
|
||||
* @param ms timeout in milliseconds. Default is 10 seconds
|
||||
* @returns a promise that rejects after the specified timeout
|
||||
*/
|
||||
export function timeoutPromise(errorMessage: string, ms: number = 10000): Promise<string> {
|
||||
return new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new TimeoutError(errorMessage));
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a unique file name
|
||||
* Increment the file name by adding 1 to function name if the file already exists
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as vscode from 'vscode';
|
||||
import * as uuid from 'uuid';
|
||||
import * as constants from '../common/constants';
|
||||
import * as utils from '../common/utils';
|
||||
import * as path from 'path';
|
||||
import * as azureFunctionsUtils from '../common/azureFunctionsUtils';
|
||||
import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
|
||||
import { addSqlBinding, getAzureFunctions } from '../services/azureFunctionsService';
|
||||
@@ -113,6 +114,13 @@ export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined):
|
||||
.withAdditionalProperties(propertyBag).send();
|
||||
return;
|
||||
}
|
||||
|
||||
// Add latest sql extension package reference to project
|
||||
// only add if AF project (.csproj) is found
|
||||
if (projectUri?.fsPath) {
|
||||
await azureFunctionsUtils.addSqlNugetReferenceToProjectFile(path.dirname(projectUri.fsPath));
|
||||
}
|
||||
|
||||
exitReason = 'done';
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.finishAddSqlBinding)
|
||||
.withAdditionalProperties(propertyBag).send();
|
||||
|
||||
@@ -26,14 +26,15 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
|
||||
TelemetryReporter.sendActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.startCreateAzureFunctionWithSqlBinding);
|
||||
let connectionInfo: IConnectionInfo | undefined;
|
||||
let isCreateNewProject: boolean = false;
|
||||
let newFunctionFileObject: azureFunctionsUtils.IFileFunctionObject | undefined;
|
||||
|
||||
try {
|
||||
// check to see if Azure Functions Extension is installed
|
||||
const azureFunctionApi = await azureFunctionsUtils.getAzureFunctionsExtensionApi();
|
||||
if (!azureFunctionApi) {
|
||||
exitReason = ExitReason.error;
|
||||
propertyBag.exitReason = exitReason;
|
||||
TelemetryReporter.createErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick)
|
||||
telemetryStep = CreateAzureFunctionStep.noAzureFunctionsExtension;
|
||||
TelemetryReporter.createErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
|
||||
.withAdditionalProperties(propertyBag).send();
|
||||
return;
|
||||
}
|
||||
@@ -70,6 +71,7 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
|
||||
{ title: constants.selectAzureFunctionProjFolder, ignoreFocusOut: true });
|
||||
if (!browseProjectLocation) {
|
||||
// User cancelled
|
||||
exitReason = ExitReason.cancelled;
|
||||
return;
|
||||
}
|
||||
const projectFolders = (await vscode.window.showOpenDialog({
|
||||
@@ -80,6 +82,7 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
|
||||
}));
|
||||
if (!projectFolders) {
|
||||
// User cancelled
|
||||
exitReason = ExitReason.cancelled;
|
||||
return;
|
||||
}
|
||||
projectFolder = projectFolders[0].fsPath;
|
||||
@@ -89,6 +92,7 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
|
||||
break;
|
||||
} else {
|
||||
// user cancelled
|
||||
exitReason = ExitReason.cancelled;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -96,9 +100,6 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
|
||||
// user has an azure function project open
|
||||
projectFolder = path.dirname(projectFile);
|
||||
}
|
||||
// create a system file watcher for the project folder
|
||||
newFunctionFileObject = azureFunctionsUtils.waitForNewFunctionFile(projectFolder);
|
||||
|
||||
// Get connection string parameters and construct object name from prompt or connectionInfo given
|
||||
let objectName: string | undefined;
|
||||
let selectedBindingType: BindingType | undefined;
|
||||
@@ -109,6 +110,7 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
|
||||
let chosenObjectType = await azureFunctionsUtils.promptForObjectType();
|
||||
if (!chosenObjectType) {
|
||||
// User cancelled
|
||||
exitReason = ExitReason.cancelled;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,6 +118,8 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
|
||||
telemetryStep = CreateAzureFunctionStep.getBindingType;
|
||||
selectedBindingType = await azureFunctionsUtils.promptForBindingType(chosenObjectType);
|
||||
if (!selectedBindingType) {
|
||||
// User cancelled
|
||||
exitReason = ExitReason.cancelled;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,6 +141,7 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
|
||||
}
|
||||
if (!connectionInfo) {
|
||||
// User cancelled
|
||||
exitReason = ExitReason.cancelled;
|
||||
return;
|
||||
}
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
|
||||
@@ -174,6 +179,8 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
|
||||
let nodeType = ObjectType.Table === node.nodeType ? ObjectType.Table : ObjectType.View;
|
||||
selectedBindingType = await azureFunctionsUtils.promptForBindingType(nodeType);
|
||||
if (!selectedBindingType) {
|
||||
// User cancelled
|
||||
exitReason = ExitReason.cancelled;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -201,6 +208,8 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
|
||||
validateInput: input => utils.validateFunctionName(input)
|
||||
}) as string;
|
||||
if (!functionName) {
|
||||
// User cancelled
|
||||
exitReason = ExitReason.cancelled;
|
||||
return;
|
||||
}
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
|
||||
@@ -219,6 +228,7 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
|
||||
connectionStringInfo = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(vscode.Uri.parse(projectFile), connectionInfo);
|
||||
if (!connectionStringInfo) {
|
||||
// User cancelled connection string setting name prompt or connection string method prompt
|
||||
exitReason = ExitReason.cancelled;
|
||||
return;
|
||||
}
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
|
||||
@@ -245,13 +255,14 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
|
||||
suppressCreateProjectPrompt: true,
|
||||
...(isCreateNewProject && { executeStep: connectionStringExecuteStep })
|
||||
});
|
||||
|
||||
// Add latest sql extension package reference to project
|
||||
await azureFunctionsUtils.addSqlNugetReferenceToProjectFile(projectFolder);
|
||||
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
|
||||
.withAdditionalProperties(propertyBag)
|
||||
.withConnectionInfo(connectionInfo).send();
|
||||
|
||||
// check for the new function file to be created and dispose of the file system watcher
|
||||
const timeoutForFunctionFile = utils.timeoutPromise(constants.timeoutAzureFunctionFileError);
|
||||
await Promise.race([newFunctionFileObject.filePromise, timeoutForFunctionFile]);
|
||||
telemetryStep = 'finishCreateFunction';
|
||||
propertyBag.telemetryStep = telemetryStep;
|
||||
exitReason = ExitReason.finishCreate;
|
||||
@@ -261,15 +272,9 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
|
||||
} catch (error) {
|
||||
let errorType = utils.getErrorType(error);
|
||||
propertyBag.telemetryStep = telemetryStep;
|
||||
if (errorType === 'TimeoutError') {
|
||||
// this error can be cause by many different scenarios including timeout or error occurred during createFunction
|
||||
exitReason = ExitReason.timeout;
|
||||
console.log('Timed out waiting for Azure Function project to be created. This may not necessarily be an error, for example if the user canceled out of the create flow.');
|
||||
} else {
|
||||
// else an error would occur during the createFunction
|
||||
exitReason = ExitReason.error;
|
||||
void vscode.window.showErrorMessage(constants.errorNewAzureFunction(error));
|
||||
}
|
||||
// an error occurred during createFunction
|
||||
exitReason = ExitReason.error;
|
||||
void vscode.window.showErrorMessage(constants.errorNewAzureFunction(error));
|
||||
TelemetryReporter.createErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick, undefined, errorType)
|
||||
.withAdditionalProperties(propertyBag).send();
|
||||
return;
|
||||
@@ -278,7 +283,6 @@ export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
|
||||
propertyBag.exitReason = exitReason;
|
||||
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick)
|
||||
.withAdditionalProperties(propertyBag).send();
|
||||
newFunctionFileObject?.watcherDisposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as should from 'should';
|
||||
import * as sinon from 'sinon';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import * as vscode from 'vscode';
|
||||
import * as azureFunctionUtils from '../../common/azureFunctionsUtils';
|
||||
import * as constants from '../../common/constants';
|
||||
@@ -90,9 +89,6 @@ describe('AzureFunctionsService', () => {
|
||||
sinon.stub(azureFunctionUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'SqlConnectionString', 'testConnectionString').resolves((true));
|
||||
sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package');
|
||||
|
||||
const testWatcher = TypeMoq.Mock.ofType<vscode.FileSystemWatcher>().object;
|
||||
sinon.stub(azureFunctionUtils, 'waitForNewFunctionFile').withArgs(sinon.match.any).returns({ filePromise: Promise.resolve('TestFileCreated'), watcherDisposable: testWatcher });
|
||||
|
||||
should(connectionInfo.database).equal('my_db', 'Initial ConnectionInfo database should be my_db');
|
||||
await azureFunctionService.createAzureFunction(tableTestNode);
|
||||
|
||||
@@ -127,9 +123,6 @@ describe('AzureFunctionsService', () => {
|
||||
sinon.stub(azureFunctionUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'SqlConnectionString', 'testConnectionString').resolves((true));
|
||||
sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package');
|
||||
|
||||
const testWatcher = TypeMoq.Mock.ofType<vscode.FileSystemWatcher>().object;
|
||||
sinon.stub(azureFunctionUtils, 'waitForNewFunctionFile').withArgs(sinon.match.any).returns({ filePromise: Promise.resolve('TestFileCreated'), watcherDisposable: testWatcher });
|
||||
|
||||
showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); // error message spy to be used for checking tests
|
||||
});
|
||||
|
||||
@@ -221,9 +214,6 @@ describe('AzureFunctionsService', () => {
|
||||
sinon.stub(azureFunctionUtils, 'setLocalAppSetting').withArgs(sinon.match.any, 'SqlConnectionString', 'testConnectionString').resolves((true));
|
||||
sinon.stub(utils, 'executeCommand').resolves('downloaded nuget package');
|
||||
|
||||
const testWatcher = TypeMoq.Mock.ofType<vscode.FileSystemWatcher>().object;
|
||||
sinon.stub(azureFunctionUtils, 'waitForNewFunctionFile').withArgs(sinon.match.any).returns({ filePromise: Promise.resolve('TestFileCreated'), watcherDisposable: testWatcher });
|
||||
|
||||
showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); // error message spy to be used for checking tests
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "sql-database-projects",
|
||||
"displayName": "SQL Database Projects",
|
||||
"description": "Enables users to develop and publish database schemas for MSSQL Databases",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"engines": {
|
||||
@@ -59,6 +59,10 @@
|
||||
"sqlDatabaseProjects.autorestSqlVersion": {
|
||||
"type": "string",
|
||||
"description": "%sqlDatabaseProjects.autorestSqlVersion%"
|
||||
},
|
||||
"sqlDatabaseProjects.collapseProjectNodes": {
|
||||
"type": "boolean",
|
||||
"description": "%sqlDatabaseProjects.collapseProjectNodes%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,5 +40,6 @@
|
||||
"sqlDatabaseProjects.netCoreDoNotAsk": "Whether to prompt the user to install .NET Core when not detected.",
|
||||
"sqlDatabaseProjects.nodejsDoNotAsk": "Whether to prompt the user to install Node.js when not detected.",
|
||||
"sqlDatabaseProjects.autorestSqlVersion": "Which version of Autorest.Sql to use from NPM. Latest will be used if not set.",
|
||||
"sqlDatabaseProjects.collapseProjectNodes": "Whether project nodes start collapsed",
|
||||
"sqlDatabaseProjects.welcome": "No database projects currently open.\n[New Project](command:sqlDatabaseProjects.new)\n[Open Project](command:sqlDatabaseProjects.open)\n[Create Project From Database](command:sqlDatabaseProjects.importDatabase)"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as path from 'path';
|
||||
import { SqlTargetPlatform } from 'sqldbproj';
|
||||
import * as utils from '../common/utils';
|
||||
|
||||
@@ -150,16 +149,18 @@ export const nameMustNotBeEmpty = localize('nameMustNotBeEmpty', "Name must not
|
||||
export const AdvancedOptionsButton = localize('advancedOptionsButton', 'Advanced...');
|
||||
export const AdvancedPublishOptions = localize('advancedPublishOptions', 'Advanced Publish Options');
|
||||
export const PublishOptions = localize('publishOptions', 'Publish Options');
|
||||
export const ExcludeObjectTypeTab = localize('excludeObjectTypes', 'Exclude Object Types');
|
||||
export const ResetButton: string = localize('reset', "Reset");
|
||||
export const OptionDescription: string = localize('optionDescription', "Option Description");
|
||||
export const OptionName: string = localize('optionName', "Option Name");
|
||||
export const OptionInclude: string = localize('Include', "Include");
|
||||
export function OptionNotFoundWarningMessage(label: string) { return localize('OptionNotFoundWarningMessage', "label: {0} does not exist in the options value name lookup", label); }
|
||||
export const OptionInclude: string = localize('include', "Include");
|
||||
export function OptionNotFoundWarningMessage(label: string) { return localize('optionNotFoundWarningMessage', "label: {0} does not exist in the options value name lookup", label); }
|
||||
|
||||
// Deploy
|
||||
export const SqlServerName = 'SQL server';
|
||||
export const AzureSqlServerName = 'Azure SQL server';
|
||||
export const SqlServerDockerImageName = 'Microsoft SQL Server';
|
||||
export const SqlServerDocker2022ImageName = 'Microsoft SQL Server 2022 (preview)';
|
||||
export const AzureSqlDbFullDockerImageName = 'Azure SQL Database emulator Full';
|
||||
export const AzureSqlDbLiteDockerImageName = 'Azure SQL Database emulator Lite';
|
||||
export const AzureSqlLogicalServerName = 'Azure SQL logical server';
|
||||
@@ -201,8 +202,8 @@ export const eulaAgreementTemplate = localize({ key: 'eulaAgreementTemplate', co
|
||||
export function eulaAgreementText(name: string) { return localize({ key: 'eulaAgreementText', comment: ['The placeholders are contents of the line and should not be translated.'] }, "I accept the {0}.", name); }
|
||||
export const eulaAgreementTitle = localize('eulaAgreementTitle', "Microsoft SQL Server License Agreement");
|
||||
export const edgeEulaAgreementTitle = localize('edgeEulaAgreementTitle', "Microsoft Azure SQL Edge License Agreement");
|
||||
export const sqlServerEulaLink = 'https://go.microsoft.com/fwlink/?linkid=857698';
|
||||
export const sqlServerEdgeEulaLink = 'https://go.microsoft.com/fwlink/?linkid=2139274';
|
||||
export const sqlServerEulaLink = 'https://aka.ms/mcr/osslegalnotice';
|
||||
export const sqlServerEdgeEulaLink = 'https://aka.ms/mcr/osslegalnotice';
|
||||
export const connectionNamePrefix = 'SQLDbProject';
|
||||
export const sqlServerDockerRegistry = 'mcr.microsoft.com';
|
||||
export const sqlServerDockerRepository = 'mssql/server';
|
||||
@@ -456,7 +457,6 @@ export const Sdk: string = 'Sdk';
|
||||
export const DatabaseSource = 'DatabaseSource';
|
||||
export const VisualStudioVersion = 'VisualStudioVersion';
|
||||
export const SSDTExists = 'SSDTExists';
|
||||
export const OutputPath = 'OutputPath';
|
||||
|
||||
export const BuildElements = localize('buildElements', "Build Elements");
|
||||
export const FolderElements = localize('folderElements', "Folder Elements");
|
||||
@@ -495,8 +495,6 @@ export const RoundTripSqlDbNotPresentCondition = '\'$(NetCoreBuild)\' != \'true\
|
||||
export const DacpacRootPath = '$(DacPacRootPath)';
|
||||
export const ProjJsonToClean = '$(BaseIntermediateOutputPath)\\project.assets.json';
|
||||
|
||||
export function defaultOutputPath() { return path.join('bin', 'Debug'); }
|
||||
|
||||
// Sqlproj VS property conditions
|
||||
export const VSVersionCondition = '\'$(VisualStudioVersion)\' == \'\'';
|
||||
export const SsdtExistsCondition = '\'$(SSDTExists)\' == \'\'';
|
||||
@@ -605,3 +603,5 @@ export enum PublishTargetType {
|
||||
docker = 'docker',
|
||||
newAzureServer = 'newAzureServer'
|
||||
}
|
||||
|
||||
export const CollapseProjectNodesKey = 'collapseProjectNodes';
|
||||
|
||||
@@ -325,13 +325,6 @@ export async function defaultAzureAccountServiceFactory(): Promise<vscodeMssql.I
|
||||
export async function getDefaultPublishDeploymentOptions(project: ISqlProject): Promise<mssql.DeploymentOptions | vscodeMssql.DeploymentOptions> {
|
||||
const schemaCompareService = await getSchemaCompareService();
|
||||
const result = await schemaCompareService.schemaCompareGetDefaultOptions();
|
||||
// re-include database-scoped credentials
|
||||
if (getAzdataApi()) {
|
||||
result.defaultDeploymentOptions.excludeObjectTypes.value = (result.defaultDeploymentOptions as mssql.DeploymentOptions).excludeObjectTypes.value?.filter(x => x !== mssql.SchemaObjectType.DatabaseScopedCredentials);
|
||||
} else {
|
||||
result.defaultDeploymentOptions.excludeObjectTypes.value = (result.defaultDeploymentOptions as vscodeMssql.DeploymentOptions).excludeObjectTypes.value?.filter(x => x !== vscodeMssql.SchemaObjectType.DatabaseScopedCredentials);
|
||||
}
|
||||
|
||||
// this option needs to be true for same database references validation to work
|
||||
if (project.databaseReferences.length > 0) {
|
||||
result.defaultDeploymentOptions.booleanOptionsDictionary.includeCompositeObjects.value = true;
|
||||
|
||||
@@ -13,11 +13,11 @@ import { SqlConnectionDataSource } from '../models/dataSources/sqlConnectionStri
|
||||
import { DeploymentOptions } from 'mssql';
|
||||
import { IconPathHelper } from '../common/iconHelper';
|
||||
import { cssStyles } from '../common/uiConstants';
|
||||
import { getAgreementDisplayText, getConnectionName, getDockerBaseImages, getPublishServerName } from './utils';
|
||||
import { getAgreementDisplayText, getConnectionName, getDefaultDockerImageWithTag, getDockerBaseImages, getPublishServerName } from './utils';
|
||||
import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
|
||||
import { Deferred } from '../common/promise';
|
||||
import { PublishOptionsDialog } from './publishOptionsDialog';
|
||||
import { ISqlProjectPublishSettings, IPublishToDockerSettings } from 'sqldbproj';
|
||||
import { ISqlProjectPublishSettings, IPublishToDockerSettings, SqlTargetPlatform } from 'sqldbproj';
|
||||
|
||||
interface DataSourceDropdownValue extends azdataType.CategoryValue {
|
||||
dataSource: SqlConnectionDataSource;
|
||||
@@ -145,7 +145,6 @@ export class PublishDatabaseDialog {
|
||||
this.connectionRow = this.createConnectionRow(view);
|
||||
this.databaseRow = this.createDatabaseRow(view);
|
||||
const displayOptionsButton = this.createOptionsButton(view);
|
||||
displayOptionsButton.enabled = false;
|
||||
|
||||
const horizontalFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
horizontalFormSection.addItems([profileRow, this.databaseRow]);
|
||||
@@ -172,12 +171,10 @@ export class PublishDatabaseDialog {
|
||||
title: constants.selectConnectionRadioButtonsTitle,
|
||||
component: selectConnectionRadioButtons
|
||||
},*/
|
||||
/* TODO : Disabling deployment options for the July release
|
||||
{
|
||||
component: displayOptionsButton,
|
||||
title: ''
|
||||
}
|
||||
*/
|
||||
]
|
||||
}
|
||||
], {
|
||||
@@ -244,9 +241,14 @@ export class PublishDatabaseDialog {
|
||||
utils.getAzdataApi()!.window.closeDialog(this.dialog);
|
||||
await this.publish!(this.project, settings);
|
||||
} else {
|
||||
const dockerBaseImage = this.getBaseDockerImageName();
|
||||
let dockerBaseImage = this.getBaseDockerImageName();
|
||||
const baseImages = getDockerBaseImages(this.project.getProjectTargetVersion());
|
||||
const imageInfo = baseImages.find(x => x.name === dockerBaseImage);
|
||||
|
||||
// selecting the image tag isn't currently exposed in the publish dialog, so this adds the tag matching the target platform
|
||||
// to make sure the correct image is used for the project's target platform when the docker base image is SQL Server
|
||||
dockerBaseImage = getDefaultDockerImageWithTag(this.project.getProjectTargetVersion(), dockerBaseImage, imageInfo);
|
||||
|
||||
const settings: IPublishToDockerSettings = {
|
||||
dockerSettings: {
|
||||
dbName: this.targetDatabaseName,
|
||||
@@ -601,6 +603,16 @@ export class PublishDatabaseDialog {
|
||||
|
||||
const baseImages = getDockerBaseImages(this.project.getProjectTargetVersion());
|
||||
const baseImagesValues: azdataType.CategoryValue[] = baseImages.map(x => { return { name: x.name, displayName: x.displayName }; });
|
||||
|
||||
// add preview string for 2022
|
||||
// TODO: remove after 2022 is GA
|
||||
if (this.project.getProjectTargetVersion() === constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2022)) {
|
||||
const sqlServerImageIndex = baseImagesValues.findIndex(image => image.displayName === constants.SqlServerDockerImageName);
|
||||
if (sqlServerImageIndex >= 0) {
|
||||
baseImagesValues[sqlServerImageIndex].displayName = constants.SqlServerDocker2022ImageName;
|
||||
}
|
||||
}
|
||||
|
||||
this.baseDockerImageDropDown = view.modelBuilder.dropDown().withProps({
|
||||
values: baseImagesValues,
|
||||
ariaLabel: constants.baseDockerImage(name),
|
||||
@@ -836,6 +848,9 @@ export class PublishDatabaseDialog {
|
||||
this.targetDatabaseName = result.databaseName;
|
||||
}
|
||||
|
||||
// set options coming from the publish profiles to deployment options
|
||||
this.setDeploymentOptions(result.options);
|
||||
|
||||
if (Object.keys(result.sqlCmdVariables).length) {
|
||||
// add SQLCMD Variables table if it wasn't there before and the profile had sqlcmd variables
|
||||
if (Object.keys(this.project.sqlCmdVariables).length === 0 && Object.keys(<Record<string, string>>this.sqlCmdVars).length === 0) {
|
||||
@@ -935,7 +950,12 @@ export class PublishDatabaseDialog {
|
||||
* Gets the default deployment options from the dacfx service
|
||||
*/
|
||||
public async getDefaultDeploymentOptions(): Promise<DeploymentOptions> {
|
||||
return await utils.getDefaultPublishDeploymentOptions(this.project) as DeploymentOptions;
|
||||
const defaultDeploymentOptions = await utils.getDefaultPublishDeploymentOptions(this.project) as DeploymentOptions;
|
||||
if (defaultDeploymentOptions && defaultDeploymentOptions.excludeObjectTypes !== undefined) {
|
||||
// For publish dialog no default exclude options should exists
|
||||
defaultDeploymentOptions.excludeObjectTypes.value = [];
|
||||
}
|
||||
return defaultDeploymentOptions;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -24,6 +24,9 @@ export class PublishOptionsDialog {
|
||||
private optionsFlexBuilder: azdataType.FlexContainer | undefined;
|
||||
private optionsChanged: boolean = false;
|
||||
private isResetOptionsClicked: boolean = false;
|
||||
private excludeObjectTypesOptionsTab: azdataType.window.DialogTab | undefined;
|
||||
private excludeObjectTypesOptionsTable: azdataType.TableComponent | undefined;
|
||||
private excludeObjectTypesOptionsFlexBuilder: azdataType.FlexContainer | undefined;
|
||||
|
||||
constructor(defaultOptions: mssql.DeploymentOptions, private publish: PublishDatabaseDialog) {
|
||||
this.optionsModel = new DeployOptionsModel(defaultOptions);
|
||||
@@ -31,8 +34,10 @@ export class PublishOptionsDialog {
|
||||
|
||||
protected initializeDialog(): void {
|
||||
this.optionsTab = utils.getAzdataApi()!.window.createTab(constants.PublishOptions);
|
||||
this.intializeDeploymentOptionsDialogTab();
|
||||
this.dialog.content = [this.optionsTab];
|
||||
this.excludeObjectTypesOptionsTab = utils.getAzdataApi()!.window.createTab(constants.ExcludeObjectTypeTab);
|
||||
this.initializeDeploymentOptionsDialogTab();
|
||||
this.initializeExcludeObjectTypesOptionsDialogTab();
|
||||
this.dialog.content = [this.optionsTab, this.excludeObjectTypesOptionsTab];
|
||||
}
|
||||
|
||||
public openDialog(): void {
|
||||
@@ -55,8 +60,26 @@ export class PublishOptionsDialog {
|
||||
utils.getAzdataApi()!.window.openDialog(this.dialog);
|
||||
}
|
||||
|
||||
private intializeDeploymentOptionsDialogTab(): void {
|
||||
private initializeDeploymentOptionsDialogTab(): void {
|
||||
this.optionsTab?.registerContent(async view => {
|
||||
// create loading component
|
||||
const loader = view.modelBuilder.loadingComponent()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'margin-top': '50%'
|
||||
}
|
||||
})
|
||||
.component();
|
||||
|
||||
this.optionsFlexBuilder = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
// adding loading component to the flexcontainer
|
||||
this.optionsFlexBuilder.addItem(loader);
|
||||
await view.initializeModel(this.optionsFlexBuilder);
|
||||
|
||||
this.descriptionHeading = view.modelBuilder.table().withProps({
|
||||
data: [],
|
||||
columns: [
|
||||
@@ -99,17 +122,41 @@ export class PublishOptionsDialog {
|
||||
}
|
||||
}));
|
||||
|
||||
this.optionsFlexBuilder = view.modelBuilder.flexContainer()
|
||||
this.optionsFlexBuilder.addItem(this.optionsTable, { CSSStyles: { 'overflow': 'scroll', 'height': '65vh', 'padding-top': '2px' } });
|
||||
this.optionsFlexBuilder.addItem(this.descriptionHeading, { CSSStyles: { 'font-weight': 'bold', 'height': '30px' } });
|
||||
this.optionsFlexBuilder.addItem(this.descriptionText, { CSSStyles: { 'padding': '4px', 'margin-right': '10px', 'overflow': 'scroll', 'height': '10vh' } });
|
||||
loader.loading = false;
|
||||
await view.initializeModel(this.optionsFlexBuilder);
|
||||
// focus the first option
|
||||
await this.optionsTable.focus();
|
||||
});
|
||||
}
|
||||
|
||||
private initializeExcludeObjectTypesOptionsDialogTab(): void {
|
||||
this.excludeObjectTypesOptionsTab?.registerContent(async view => {
|
||||
this.excludeObjectTypesOptionsTable = view.modelBuilder.table().component();
|
||||
await this.updateExcludeObjectsTable();
|
||||
|
||||
// Update exclude type options value on checkbox onchange
|
||||
this.disposableListeners.push(this.excludeObjectTypesOptionsTable!.onCellAction!((rowState) => {
|
||||
const checkboxState = <azdataType.ICheckboxCellActionEventArgs>rowState;
|
||||
if (checkboxState && checkboxState.row !== undefined) {
|
||||
// data[row][1] contains the exclude type option display name
|
||||
const displayName = this.excludeObjectTypesOptionsTable?.data[checkboxState.row][1];
|
||||
this.optionsModel.setExcludeObjectTypesOptionValue(displayName, checkboxState.checked);
|
||||
this.optionsChanged = true;
|
||||
// customButton[0] is the reset button, enabling it when option checkbox is changed
|
||||
this.dialog.customButtons[0].enabled = true;
|
||||
}
|
||||
}));
|
||||
|
||||
this.excludeObjectTypesOptionsFlexBuilder = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
this.optionsFlexBuilder.addItem(this.optionsTable, { CSSStyles: { 'overflow': 'scroll', 'height': '65vh', 'padding-top': '2px' } });
|
||||
this.optionsFlexBuilder.addItem(this.descriptionHeading, { CSSStyles: { 'font-weight': 'bold', 'height': '30px' } });
|
||||
this.optionsFlexBuilder.addItem(this.descriptionText, { CSSStyles: { 'padding': '4px', 'margin-right': '10px', 'overflow': 'scroll', 'height': '10vh' } });
|
||||
await view.initializeModel(this.optionsFlexBuilder);
|
||||
// focus the first option
|
||||
await this.optionsTable.focus();
|
||||
this.excludeObjectTypesOptionsFlexBuilder.addItem(this.excludeObjectTypesOptionsTable, { CSSStyles: { 'overflow': 'scroll', 'height': '80vh', 'padding-top': '2px' } });
|
||||
await view.initializeModel(this.excludeObjectTypesOptionsFlexBuilder);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -141,12 +188,41 @@ export class PublishOptionsDialog {
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Update the default options to the exclude objects table area
|
||||
*/
|
||||
private async updateExcludeObjectsTable(): Promise<void> {
|
||||
const data = this.optionsModel.getExcludeObjectTypesOptionsData();
|
||||
await this.excludeObjectTypesOptionsTable?.updateProperties({
|
||||
data: data,
|
||||
columns: [
|
||||
<azdataType.CheckboxColumn>
|
||||
{
|
||||
value: constants.OptionInclude,
|
||||
type: utils.getAzdataApi()!.ColumnType.checkBox,
|
||||
action: utils.getAzdataApi()!.ActionOnCellCheckboxCheck.customAction,
|
||||
headerCssClass: 'display-none',
|
||||
cssClass: 'no-borders align-with-header',
|
||||
width: 50
|
||||
},
|
||||
{
|
||||
value: constants.OptionName,
|
||||
headerCssClass: 'display-none',
|
||||
cssClass: 'no-borders align-with-header',
|
||||
width: 50
|
||||
}
|
||||
],
|
||||
ariaRowCount: data.length
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Ok button click, will update the deployment options with selections
|
||||
*/
|
||||
protected execute(): void {
|
||||
// Update the model deploymentoptions with the updated table component values
|
||||
// Update the model deploymentoptions with the updated options/excludeObjects table component values
|
||||
this.optionsModel.setDeploymentOptions();
|
||||
this.optionsModel.setExcludeObjectTypesToDeploymentOptions();
|
||||
// Set the publish deploymentoptions with the updated table component values
|
||||
this.publish.setDeploymentOptions(this.optionsModel.deploymentOptions);
|
||||
this.disposeListeners();
|
||||
@@ -173,14 +249,19 @@ export class PublishOptionsDialog {
|
||||
const result = await this.publish.getDefaultDeploymentOptions();
|
||||
this.optionsModel.deploymentOptions = result;
|
||||
|
||||
// reset optionsvalueNameLookup with default deployment options
|
||||
// reset optionsvalueNameLookup and excludeObjectTypesLookup with default deployment options
|
||||
this.optionsModel.setOptionsToValueNameLookup();
|
||||
this.optionsModel.setExcludeObjectTypesLookup();
|
||||
|
||||
await this.updateOptionsTable();
|
||||
this.optionsFlexBuilder?.removeItem(this.optionsTable!);
|
||||
this.optionsFlexBuilder?.insertItem(this.optionsTable!, 0, { CSSStyles: { 'overflow': 'scroll', 'height': '65vh', 'padding-top': '2px' } });
|
||||
TelemetryReporter.sendActionEvent(TelemetryViews.PublishOptionsDialog, TelemetryActions.resetOptions);
|
||||
|
||||
await this.updateExcludeObjectsTable();
|
||||
this.excludeObjectTypesOptionsFlexBuilder?.removeItem(this.excludeObjectTypesOptionsTable!);
|
||||
this.excludeObjectTypesOptionsFlexBuilder?.addItem(this.excludeObjectTypesOptionsTable!, { CSSStyles: { 'overflow': 'scroll', 'height': '80vh', 'padding-top': '2px' } });
|
||||
|
||||
// setting optionsChanged to false when reset click, if optionsChanged is true during execute, that means there is an option changed after reset
|
||||
this.isResetOptionsClicked = true;
|
||||
this.optionsChanged = false;
|
||||
|
||||
@@ -155,3 +155,32 @@ export function getDockerBaseImages(target: string): DockerImageInfo[] {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This adds the tag matching the target platform to make sure the correct image is used for the project's target platform when the docker base image is SQL Server.
|
||||
* If the image is Edge, then no tag is appended
|
||||
* @param projectTargetVersion target version of the project
|
||||
* @param dockerImage selected base docker image without tag
|
||||
* @param imageInfo docker image info of the selected docker image
|
||||
* @returns dockerBaseImage with the appropriate image tag appended if there is one
|
||||
*/
|
||||
export function getDefaultDockerImageWithTag(projectTargetVersion: string, dockerImage: string, imageInfo?: DockerImageInfo,): string {
|
||||
if (imageInfo?.displayName === constants.SqlServerDockerImageName) {
|
||||
switch (projectTargetVersion) {
|
||||
case constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2022):
|
||||
dockerImage = `${dockerImage}:2022-latest`;
|
||||
break;
|
||||
case constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2019):
|
||||
dockerImage = `${dockerImage}:2019-latest`;
|
||||
break;
|
||||
case constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2017):
|
||||
dockerImage = `${dockerImage}:2017-latest`;
|
||||
break;
|
||||
default:
|
||||
// nothing - let it be the default image defined as default in the container registry
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return dockerImage;
|
||||
}
|
||||
|
||||
@@ -10,9 +10,11 @@ import * as constants from '../../common/constants';
|
||||
export class DeployOptionsModel {
|
||||
// key is the option display name and values are checkboxValue and optionName
|
||||
private optionsValueNameLookup: { [key: string]: mssql.IOptionWithValue } = {};
|
||||
private excludeObjectTypesLookup: { [key: string]: mssql.IOptionWithValue } = {};
|
||||
|
||||
constructor(public deploymentOptions: mssql.DeploymentOptions) {
|
||||
this.setOptionsToValueNameLookup();
|
||||
this.setExcludeObjectTypesLookup();
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -69,4 +71,62 @@ export class DeployOptionsModel {
|
||||
}
|
||||
return optionName !== undefined ? this.deploymentOptions.booleanOptionsDictionary[optionName.optionName].description : '';
|
||||
}
|
||||
|
||||
/*
|
||||
* Sets exclude object types option's checkbox values and property name to the excludeObjectTypesLookup map
|
||||
*/
|
||||
public setExcludeObjectTypesLookup(): void {
|
||||
Object.entries(this.deploymentOptions.objectTypesDictionary).forEach(option => {
|
||||
const optionValue: mssql.IOptionWithValue = {
|
||||
optionName: option[0],
|
||||
checked: this.getExcludeObjectTypeOptionCheckStatus(option[0])
|
||||
};
|
||||
this.excludeObjectTypesLookup[option[1]] = optionValue;
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Initialize options data from objectTypesDictionary for table component
|
||||
* Returns data as [booleanValue, optionName]
|
||||
*/
|
||||
public getExcludeObjectTypesOptionsData(): any[][] {
|
||||
let data: any[][] = [];
|
||||
Object.entries(this.deploymentOptions.objectTypesDictionary).forEach(option => {
|
||||
// option[1] is the display name and option[0] is the optionName
|
||||
data.push([this.getExcludeObjectTypeOptionCheckStatus(option[0]), option[1]]);
|
||||
});
|
||||
|
||||
return data.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets the selected/default value of the object type option
|
||||
* return true for the deploymentOptions.excludeObjectTypes option, if it is in ObjectTypesDictionary
|
||||
*/
|
||||
public getExcludeObjectTypeOptionCheckStatus(optionName: string): boolean {
|
||||
return (this.deploymentOptions.excludeObjectTypes.value?.find(x => x.toLowerCase() === optionName.toLowerCase())) !== undefined ? true : false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sets the checkbox value to the excludeObjectTypesLookup map
|
||||
*/
|
||||
public setExcludeObjectTypesOptionValue(displayName: string, checked: boolean): void {
|
||||
this.excludeObjectTypesLookup[displayName].checked = checked;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sets the selected option checkbox value to the deployment options
|
||||
*/
|
||||
public setExcludeObjectTypesToDeploymentOptions(): void {
|
||||
let finalExcludedObjectTypes: string[] = [];
|
||||
Object.entries(this.excludeObjectTypesLookup).forEach(option => {
|
||||
// option[1] holds checkedbox value and optionName
|
||||
if (option[1].checked) {
|
||||
finalExcludedObjectTypes.push(option[1].optionName);
|
||||
}
|
||||
});
|
||||
|
||||
this.deploymentOptions.excludeObjectTypes.value = finalExcludedObjectTypes;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,10 +36,9 @@ export class Project implements ISqlProject {
|
||||
private _postDeployScripts: FileProjectEntry[] = [];
|
||||
private _noneDeployScripts: FileProjectEntry[] = [];
|
||||
private _isSdkStyleProject: boolean = false; // https://docs.microsoft.com/en-us/dotnet/core/project-sdk/overview
|
||||
private _outputPath: string = '';
|
||||
|
||||
public get dacpacOutputPath(): string {
|
||||
return path.join(this.outputPath, `${this._projectFileName}.dacpac`);
|
||||
return path.join(this.projectFolderPath, 'bin', 'Debug', `${this._projectFileName}.dacpac`);
|
||||
}
|
||||
|
||||
public get projectFolderPath() {
|
||||
@@ -94,10 +93,6 @@ export class Project implements ISqlProject {
|
||||
return this._isSdkStyleProject;
|
||||
}
|
||||
|
||||
public get outputPath(): string {
|
||||
return this._outputPath;
|
||||
}
|
||||
|
||||
private projFileXmlDoc: Document | undefined = undefined;
|
||||
|
||||
constructor(projectFilePath: string) {
|
||||
@@ -160,16 +155,6 @@ export class Project implements ISqlProject {
|
||||
this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.PropertyGroup)[0]?.appendChild(newProjectGuidNode);
|
||||
await this.serializeToProjFile(this.projFileXmlDoc);
|
||||
}
|
||||
|
||||
// get output path
|
||||
const outputNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.OutputPath);
|
||||
if (outputNodes.length > 0) {
|
||||
const outputPath = outputNodes[0].childNodes[0].nodeValue!;
|
||||
this._outputPath = path.join(utils.getPlatformSafeFileEntryPath(this.projectFolderPath), utils.getPlatformSafeFileEntryPath(outputPath));
|
||||
} else {
|
||||
// If output path isn't specified in .sqlproj, set it to the default output path .\bin\Debug\
|
||||
this._outputPath = path.join(utils.getPlatformSafeFileEntryPath(this.projectFolderPath), utils.getPlatformSafeFileEntryPath(constants.defaultOutputPath()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,10 +11,11 @@ import * as fileTree from './fileFolderTreeItem';
|
||||
import { Project } from '../project';
|
||||
import * as utils from '../../common/utils';
|
||||
import { DatabaseReferencesTreeItem } from './databaseReferencesTreeItem';
|
||||
import { DatabaseProjectItemType, RelativeOuterPath, ExternalStreamingJob, sqlprojExtension } from '../../common/constants';
|
||||
import { DatabaseProjectItemType, RelativeOuterPath, ExternalStreamingJob, sqlprojExtension, CollapseProjectNodesKey } from '../../common/constants';
|
||||
import { IconPathHelper } from '../../common/iconHelper';
|
||||
import { FileProjectEntry } from '../projectEntry';
|
||||
import { EntryType } from 'sqldbproj';
|
||||
import { DBProjectConfigurationKey } from '../../tools/netcoreTool';
|
||||
|
||||
/**
|
||||
* TreeNode root that represents an entire project
|
||||
@@ -47,7 +48,8 @@ export class ProjectRootTreeItem extends BaseProjectTreeItem {
|
||||
}
|
||||
|
||||
public get treeItem(): vscode.TreeItem {
|
||||
const projectItem = new vscode.TreeItem(this.fileSystemUri, vscode.TreeItemCollapsibleState.Expanded);
|
||||
const collapsibleState = vscode.workspace.getConfiguration(DBProjectConfigurationKey)[CollapseProjectNodesKey] ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.Expanded;
|
||||
const projectItem = new vscode.TreeItem(this.fileSystemUri, collapsibleState);
|
||||
projectItem.contextValue = this.project.isSdkStyleProject ? DatabaseProjectItemType.project : DatabaseProjectItemType.legacyProject;
|
||||
projectItem.iconPath = IconPathHelper.databaseProject;
|
||||
projectItem.label = path.basename(this.projectUri.fsPath, sqlprojExtension);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<ProjectGuid>{2C283C5D-9E4A-4313-8FF9-4E0CEE20B063}</ProjectGuid>
|
||||
<DSP>Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider</DSP>
|
||||
<ModelCollation>1033, CI</ModelCollation>
|
||||
<OutputPath>..\otherFolder</OutputPath>
|
||||
</PropertyGroup>
|
||||
<Target Name="BeforeBuild">
|
||||
<Delete Files="$(BaseIntermediateOutputPath)\project.assets.json" />
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as should from 'should';
|
||||
import * as constants from '../../common/constants';
|
||||
import { SqlTargetPlatform } from 'sqldbproj';
|
||||
import { getDefaultDockerImageWithTag, getDockerBaseImages } from '../../dialogs/utils';
|
||||
|
||||
describe('Tests to verify dialog utils functions', function (): void {
|
||||
it('getDefaultDockerImageWithTag should return correct image', () => {
|
||||
const baseImages = getDockerBaseImages(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2022)!);
|
||||
const sqlServerImageInfo = baseImages.find(image => image.displayName === constants.SqlServerDockerImageName);
|
||||
const edgeImageInfo = baseImages.find(image => image.displayName === SqlTargetPlatform.sqlEdge);
|
||||
|
||||
should(getDefaultDockerImageWithTag('160', 'mcr.microsoft.com/mssql/server', sqlServerImageInfo)).equals(`${sqlServerImageInfo?.name}:2022-latest`, 'Unexpected docker image returned for target platform SQL Server 2022 and SQL Server base image');
|
||||
should(getDefaultDockerImageWithTag('150', 'mcr.microsoft.com/mssql/server', sqlServerImageInfo)).equals(`${sqlServerImageInfo?.name}:2019-latest`, 'Unexpected docker image returned for target platform SQL Server 2019 and SQL Server base image');
|
||||
should(getDefaultDockerImageWithTag('140', 'mcr.microsoft.com/mssql/server', sqlServerImageInfo)).equals(`${sqlServerImageInfo?.name}:2017-latest`, 'Unexpected docker image returned for target platform SQL Server 2017 and SQL Server base image');
|
||||
should(getDefaultDockerImageWithTag('130', 'mcr.microsoft.com/mssql/server', sqlServerImageInfo)).equals(`${sqlServerImageInfo?.name}`, 'Unexpected docker image returned for target platform SQL Server 2016 and SQL Server base image');
|
||||
should(getDefaultDockerImageWithTag('150', 'mcr.microsoft.com/azure-sql-edge', edgeImageInfo)).equals(`${edgeImageInfo?.name}`, 'Unexpected docker image returned for target platform SQL Server 2019 and Edge base image');
|
||||
|
||||
// different display names are returned when a project's target platform is Azure, but currently the Azure full image points to mcr.microsoft.com/mssql/server
|
||||
const azureBaseImages = getDockerBaseImages(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlAzure)!);
|
||||
const azureFullImageInfo = azureBaseImages.find(image => image.displayName === constants.AzureSqlDbFullDockerImageName);
|
||||
const azureLiteImageInfo = azureBaseImages.find(image => image.displayName === constants.AzureSqlDbLiteDockerImageName);
|
||||
|
||||
should(getDefaultDockerImageWithTag('AzureV12', 'mcr.microsoft.com/mssql/server', azureFullImageInfo)).equals(`${azureFullImageInfo?.name}`, 'Unexpected docker image returned for target platform Azure and Azure full base image');
|
||||
should(getDefaultDockerImageWithTag('AzureV12', 'mcr.microsoft.com/azure-sql-edge', azureLiteImageInfo)).equals(`${azureLiteImageInfo?.name}`, 'Unexpected docker image returned for target platform Azure Azure lite base image');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('Publish Dialog Deploy Options Model', () => {
|
||||
const model = new DeployOptionsModel(testUtils.getDeploymentOptions());
|
||||
Object.entries(model.deploymentOptions.booleanOptionsDictionary).forEach(option => {
|
||||
// option[1] contains the value, description and displayName
|
||||
should(model.getOptionDescription(option[1].displayName)).not.equal(undefined);
|
||||
should(model.getOptionDescription(option[1].displayName)).not.equal(undefined, 'publish option description should not be undefined');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,4 +27,31 @@ describe('Publish Dialog Deploy Options Model', () => {
|
||||
const model = new DeployOptionsModel(testUtils.getDeploymentOptions());
|
||||
should(model.getOptionDescription('')).equal('');
|
||||
});
|
||||
|
||||
|
||||
it('Should have no default exclude object types', function (): void {
|
||||
const model = new DeployOptionsModel(testUtils.getDeploymentOptions());
|
||||
should(model.deploymentOptions.excludeObjectTypes.value.length).be.equal(0, 'There should be no object types excluded from excludeObjectTypes');
|
||||
|
||||
// should return true for all object type options as there are no default excludeObjectTypes in the deployment options
|
||||
Object.keys(model.deploymentOptions.objectTypesDictionary).forEach(option => {
|
||||
should(model.getExcludeObjectTypeOptionCheckStatus(option)).equal(false, 'excludeObjectTypes property should be empty by default and return false');
|
||||
});
|
||||
});
|
||||
|
||||
it('Should have atleast one default exclude object types', function (): void {
|
||||
const model = new DeployOptionsModel(testUtils.getDeploymentOptions());
|
||||
model.deploymentOptions.excludeObjectTypes.value = ['SampleProperty1'];
|
||||
|
||||
should(model.deploymentOptions.excludeObjectTypes.value.length).be.equal(1, 'There should be one excluded object');
|
||||
|
||||
// should return true for all exclude object types options and false for the exising defauit option
|
||||
Object.keys(model.deploymentOptions.objectTypesDictionary).forEach(option => {
|
||||
if (option === 'SampleProperty1') {
|
||||
should(model.getExcludeObjectTypeOptionCheckStatus(option)).equal(true, 'should return true for the excludeObjectTypes SampleProperty1 ');
|
||||
} else {
|
||||
should(model.getExcludeObjectTypeOptionCheckStatus(option)).equal(false, 'should return false for all excludeObjectTypes property as it is empty');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import * as constants from '../common/constants';
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import { Project } from '../models/project';
|
||||
import { exists, convertSlashesForSqlProj, getWellKnownDatabaseSources, getPlatformSafeFileEntryPath } from '../common/utils';
|
||||
import { exists, convertSlashesForSqlProj, getWellKnownDatabaseSources } from '../common/utils';
|
||||
import { Uri, window } from 'vscode';
|
||||
import { IDacpacReferenceSettings, IProjectReferenceSettings, ISystemDatabaseReferenceSettings } from '../models/IDatabaseReferenceSettings';
|
||||
import { EntryType, ItemType, SqlTargetPlatform } from 'sqldbproj';
|
||||
@@ -1430,30 +1430,6 @@ describe('Project: sdk style project content operations', function (): void {
|
||||
should(projFileText.includes(constants.ProjectGuid)).equal(true);
|
||||
});
|
||||
|
||||
it('Should read OutputPath from sqlproj if there is one', async function (): Promise<void> {
|
||||
projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectBaseline);
|
||||
const projFileText = (await fs.readFile(projFilePath)).toString();
|
||||
|
||||
// Verify sqlproj has OutputPath
|
||||
should(projFileText.includes(constants.OutputPath)).equal(true);
|
||||
|
||||
const project: Project = await Project.openProject(projFilePath);
|
||||
should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('..\\otherFolder')));
|
||||
should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('..\\otherFolder'), `${project.projectFileName}.dacpac`));
|
||||
});
|
||||
|
||||
it('Should use default output path if OutputPath is not specified in sqlproj', async function (): Promise<void> {
|
||||
projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectWithGlobsSpecifiedBaseline);
|
||||
const projFileText = (await fs.readFile(projFilePath)).toString();
|
||||
|
||||
// Verify sqlproj doesn't have OutputPath
|
||||
should(projFileText.includes(constants.OutputPath)).equal(true);
|
||||
|
||||
const project: Project = await Project.openProject(projFilePath);
|
||||
should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath(constants.defaultOutputPath())));
|
||||
should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath(constants.defaultOutputPath()), `${project.projectFileName}.dacpac`));
|
||||
});
|
||||
|
||||
it('Should handle adding existing items to project', async function (): Promise<void> {
|
||||
projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectBaseline);
|
||||
const projectFolder = path.dirname(projFilePath);
|
||||
|
||||
@@ -31,6 +31,10 @@ export function getDeploymentOptions(): mssql.DeploymentOptions {
|
||||
booleanOptionsDictionary: {
|
||||
'SampleProperty1': { value: false, description: sampleDesc, displayName: sampleName },
|
||||
'SampleProperty2': { value: false, description: sampleDesc, displayName: sampleName }
|
||||
},
|
||||
objectTypesDictionary: {
|
||||
'SampleProperty1': sampleName,
|
||||
'SampleProperty2': sampleName
|
||||
}
|
||||
};
|
||||
return defaultOptions;
|
||||
|
||||
3
extensions/sql-migration/images/addNew.svg
Normal file
3
extensions/sql-migration/images/addNew.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.999 7.5V8.5H7.99902V15.5H6.99902V8.5H-0.000976562V7.5H6.99902V0.5H7.99902V7.5H14.999Z" fill="#0078D4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 220 B |
3
extensions/sql-migration/images/allTables.svg
Normal file
3
extensions/sql-migration/images/allTables.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="M1 2H14V14H1V2ZM2 4H13V3H2V4ZM5 7V5H2V7H5ZM2 8V10H5V8H2ZM5 13V11H2V13H5ZM9 7V5H6V7H9ZM6 8V10H9V8H6ZM9 13V11H6V13H9ZM13 7V5H10V7H13ZM13 10V8H10V10H13ZM13 13V11H10V13H13Z" fill="#323130"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 299 B |
3
extensions/sql-migration/images/breadCrumb.svg
Normal file
3
extensions/sql-migration/images/breadCrumb.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<defs><style>.cls-1{fill:#231f20;}</style></defs><title>chevron_right</title><polygon class="cls-1" points="13.59 2.21 13.58 2.22 13.58 2.2 13.59 2.21"/><path d="M10.54,8l-7-7,1-1,8,8-8,8-1-1Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 299 B |
@@ -2,7 +2,7 @@
|
||||
"name": "sql-migration",
|
||||
"displayName": "%displayName%",
|
||||
"description": "%description%",
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.5",
|
||||
"publisher": "Microsoft",
|
||||
"preview": false,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
@@ -147,32 +147,7 @@
|
||||
"when": "connectionProvider == 'MSSQL' && !mssql:iscloud && mssql:engineedition != 8",
|
||||
"hideRefreshTask": true,
|
||||
"container": {
|
||||
"grid-container": [
|
||||
{
|
||||
"name": "",
|
||||
"row": 0,
|
||||
"col": 0,
|
||||
"widget": {
|
||||
"tasks-widget": [
|
||||
"sqlmigration.start",
|
||||
"sqlmigration.newsupportrequest",
|
||||
"sqlmigration.sendfeedback"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"row": 0,
|
||||
"col": 1,
|
||||
"rowspan": 2.5,
|
||||
"colspan": 3.5,
|
||||
"widget": {
|
||||
"modelview": {
|
||||
"id": "migration.dashboard"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
"modelview-container": null
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -526,6 +526,22 @@ export interface MigrationStatusDetails {
|
||||
lastRestoredFilename: string;
|
||||
pendingLogBackupsCount: number;
|
||||
invalidFiles: string[];
|
||||
listOfCopyProgressDetails: CopyProgressDetail[];
|
||||
}
|
||||
|
||||
export interface CopyProgressDetail {
|
||||
tableName: string;
|
||||
status: 'PreparingForCopy' | 'Copying' | 'CopyFinished' | 'RebuildingIndexes' | 'Succeeded' | 'Failed' | 'Canceled',
|
||||
parallelCopyType: string;
|
||||
usedParallelCopies: number;
|
||||
dataRead: number;
|
||||
dataWritten: number;
|
||||
rowsRead: number;
|
||||
rowsCopied: number;
|
||||
copyStart: string;
|
||||
copyThroughput: number,
|
||||
copyDuration: number;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export interface SqlConnectionInfo {
|
||||
|
||||
@@ -5,13 +5,16 @@
|
||||
|
||||
import { window, Account, accounts, CategoryValue, DropDownComponent, IconPath } from 'azdata';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel';
|
||||
import { MigrationStatus, ProvisioningState } from '../models/migrationLocalStorage';
|
||||
import * as crypto from 'crypto';
|
||||
import * as azure from './azure';
|
||||
import { azureResource, Tenant } from 'azurecore';
|
||||
import * as constants from '../constants/strings';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import { AdsMigrationStatus } from '../dashboard/tabBase';
|
||||
import { getMigrationMode, getMigrationStatus, getMigrationTargetType, PipelineStatusCodes } from '../constants/helper';
|
||||
|
||||
export const DefaultSettingValue = '---';
|
||||
|
||||
export function deepClone<T>(obj: T): T {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
@@ -92,34 +95,68 @@ export function convertTimeDifferenceToDuration(startTime: Date, endTime: Date):
|
||||
}
|
||||
}
|
||||
|
||||
export function filterMigrations(databaseMigrations: azure.DatabaseMigration[], statusFilter: string, databaseNameFilter?: string): azure.DatabaseMigration[] {
|
||||
let filteredMigration: azure.DatabaseMigration[] = [];
|
||||
if (statusFilter === AdsMigrationStatus.ALL) {
|
||||
filteredMigration = databaseMigrations;
|
||||
} else if (statusFilter === AdsMigrationStatus.ONGOING) {
|
||||
filteredMigration = databaseMigrations.filter(
|
||||
value => {
|
||||
const status = value.properties?.migrationStatus;
|
||||
return status === MigrationStatus.InProgress
|
||||
|| status === MigrationStatus.Creating
|
||||
|| value.properties?.provisioningState === MigrationStatus.Creating;
|
||||
});
|
||||
} else if (statusFilter === AdsMigrationStatus.SUCCEEDED) {
|
||||
filteredMigration = databaseMigrations.filter(
|
||||
value => value.properties?.migrationStatus === MigrationStatus.Succeeded);
|
||||
} else if (statusFilter === AdsMigrationStatus.FAILED) {
|
||||
filteredMigration = databaseMigrations.filter(
|
||||
value =>
|
||||
value.properties?.migrationStatus === MigrationStatus.Failed ||
|
||||
value.properties?.provisioningState === ProvisioningState.Failed);
|
||||
} else if (statusFilter === AdsMigrationStatus.COMPLETING) {
|
||||
filteredMigration = databaseMigrations.filter(
|
||||
value => value.properties?.migrationStatus === MigrationStatus.Completing);
|
||||
export function getMigrationTime(migrationTime: string): string {
|
||||
return migrationTime
|
||||
? new Date(migrationTime).toLocaleString()
|
||||
: DefaultSettingValue;
|
||||
}
|
||||
|
||||
export function getMigrationDuration(startDate: string, endDate: string): string {
|
||||
if (startDate) {
|
||||
if (endDate) {
|
||||
return convertTimeDifferenceToDuration(
|
||||
new Date(startDate),
|
||||
new Date(endDate));
|
||||
} else {
|
||||
return convertTimeDifferenceToDuration(
|
||||
new Date(startDate),
|
||||
new Date());
|
||||
}
|
||||
}
|
||||
if (databaseNameFilter) {
|
||||
const filter = databaseNameFilter.toLowerCase();
|
||||
|
||||
return DefaultSettingValue;
|
||||
}
|
||||
|
||||
export function filterMigrations(databaseMigrations: azure.DatabaseMigration[], statusFilter: string, columnTextFilter?: string): azure.DatabaseMigration[] {
|
||||
let filteredMigration: azure.DatabaseMigration[] = databaseMigrations || [];
|
||||
if (columnTextFilter) {
|
||||
const filter = columnTextFilter.toLowerCase();
|
||||
filteredMigration = filteredMigration.filter(
|
||||
migration => migration.name?.toLowerCase().includes(filter));
|
||||
migration => migration.properties.sourceServerName?.toLowerCase().includes(filter)
|
||||
|| migration.properties.sourceDatabaseName?.toLowerCase().includes(filter)
|
||||
|| getMigrationStatus(migration)?.toLowerCase().includes(filter)
|
||||
|| getMigrationMode(migration)?.toLowerCase().includes(filter)
|
||||
|| getMigrationTargetType(migration)?.toLowerCase().includes(filter)
|
||||
|| azure.getResourceName(migration.properties.scope)?.toLowerCase().includes(filter)
|
||||
|| azure.getResourceName(migration.id)?.toLowerCase().includes(filter)
|
||||
|| getMigrationDuration(
|
||||
migration.properties.startedOn,
|
||||
migration.properties.endedOn)?.toLowerCase().includes(filter)
|
||||
|| getMigrationTime(migration.properties.startedOn)?.toLowerCase().includes(filter)
|
||||
|| getMigrationTime(migration.properties.endedOn)?.toLowerCase().includes(filter)
|
||||
|| getMigrationMode(migration)?.toLowerCase().includes(filter));
|
||||
}
|
||||
|
||||
switch (statusFilter) {
|
||||
case AdsMigrationStatus.ALL:
|
||||
return filteredMigration;
|
||||
case AdsMigrationStatus.ONGOING:
|
||||
return filteredMigration.filter(
|
||||
value => {
|
||||
const status = getMigrationStatus(value);
|
||||
return status === MigrationStatus.InProgress
|
||||
|| status === MigrationStatus.Retriable
|
||||
|| status === MigrationStatus.Creating;
|
||||
});
|
||||
case AdsMigrationStatus.SUCCEEDED:
|
||||
return filteredMigration.filter(
|
||||
value => getMigrationStatus(value) === MigrationStatus.Succeeded);
|
||||
case AdsMigrationStatus.FAILED:
|
||||
return filteredMigration.filter(
|
||||
value => getMigrationStatus(value) === MigrationStatus.Failed);
|
||||
case AdsMigrationStatus.COMPLETING:
|
||||
return filteredMigration.filter(
|
||||
value => getMigrationStatus(value) === MigrationStatus.Completing);
|
||||
}
|
||||
return filteredMigration;
|
||||
}
|
||||
@@ -208,12 +245,61 @@ export function decorate(decorator: (fn: Function, key: string) => Function): Fu
|
||||
}
|
||||
|
||||
export function getSessionIdHeader(sessionId: string): { [key: string]: string } {
|
||||
return {
|
||||
'SqlMigrationSessionId': sessionId
|
||||
};
|
||||
return { 'SqlMigrationSessionId': sessionId };
|
||||
}
|
||||
|
||||
export function getMigrationStatusImage(status: string): IconPath {
|
||||
export function getMigrationStatusWithErrors(migration: azure.DatabaseMigration): string {
|
||||
const properties = migration.properties;
|
||||
const migrationStatus = properties.migrationStatus ?? properties.provisioningState;
|
||||
let warningCount = 0;
|
||||
|
||||
if (properties.migrationFailureError?.message) {
|
||||
warningCount++;
|
||||
}
|
||||
if (properties.migrationStatusDetails?.fileUploadBlockingErrors) {
|
||||
const blockingErrors = properties.migrationStatusDetails?.fileUploadBlockingErrors.length ?? 0;
|
||||
warningCount += blockingErrors;
|
||||
}
|
||||
if (properties.migrationStatusDetails?.restoreBlockingReason) {
|
||||
warningCount++;
|
||||
}
|
||||
|
||||
return constants.STATUS_VALUE(migrationStatus, warningCount)
|
||||
+ (constants.STATUS_WARNING_COUNT(migrationStatus, warningCount) ?? '');
|
||||
}
|
||||
|
||||
export function getPipelineStatusImage(status: string | undefined): IconPath {
|
||||
// status codes: 'PreparingForCopy' | 'Copying' | 'CopyFinished' | 'RebuildingIndexes' | 'Succeeded' | 'Failed' | 'Canceled',
|
||||
switch (status) {
|
||||
case PipelineStatusCodes.Copying: // Copying: 'Copying',
|
||||
return IconPathHelper.copy;
|
||||
case PipelineStatusCodes.CopyFinished: // CopyFinished: 'CopyFinished',
|
||||
case PipelineStatusCodes.RebuildingIndexes: // RebuildingIndexes: 'RebuildingIndexes',
|
||||
return IconPathHelper.inProgressMigration;
|
||||
case PipelineStatusCodes.Canceled: // Canceled: 'Canceled',
|
||||
return IconPathHelper.cancel;
|
||||
case PipelineStatusCodes.PreparingForCopy: // PreparingForCopy: 'PreparingForCopy',
|
||||
return IconPathHelper.notStartedMigration;
|
||||
case PipelineStatusCodes.Failed: // Failed: 'Failed',
|
||||
return IconPathHelper.error;
|
||||
case PipelineStatusCodes.Succeeded: // Succeeded: 'Succeeded',
|
||||
return IconPathHelper.completedMigration;
|
||||
|
||||
// legacy status codes: Queued: 'Queued', InProgress: 'InProgress',Cancelled: 'Cancelled',
|
||||
case PipelineStatusCodes.Queued:
|
||||
return IconPathHelper.notStartedMigration;
|
||||
case PipelineStatusCodes.InProgress:
|
||||
return IconPathHelper.inProgressMigration;
|
||||
case PipelineStatusCodes.Cancelled:
|
||||
return IconPathHelper.cancel;
|
||||
// default:
|
||||
default:
|
||||
return IconPathHelper.error;
|
||||
}
|
||||
}
|
||||
|
||||
export function getMigrationStatusImage(migration: azure.DatabaseMigration): IconPath {
|
||||
const status = getMigrationStatus(migration);
|
||||
switch (status) {
|
||||
case MigrationStatus.InProgress:
|
||||
return IconPathHelper.inProgressMigration;
|
||||
@@ -223,7 +309,10 @@ export function getMigrationStatusImage(status: string): IconPath {
|
||||
return IconPathHelper.notStartedMigration;
|
||||
case MigrationStatus.Completing:
|
||||
return IconPathHelper.completingCutover;
|
||||
case MigrationStatus.Retriable:
|
||||
return IconPathHelper.retry;
|
||||
case MigrationStatus.Canceling:
|
||||
case MigrationStatus.Canceled:
|
||||
return IconPathHelper.cancel;
|
||||
case MigrationStatus.Failed:
|
||||
default:
|
||||
|
||||
@@ -15,8 +15,84 @@ export enum SQLTargetAssetType {
|
||||
SQLDB = 'Microsoft.Sql/servers',
|
||||
}
|
||||
|
||||
export function getMigrationTargetType(migration: DatabaseMigration): string {
|
||||
const id = migration.id?.toLowerCase();
|
||||
export const ParallelCopyTypeCodes = {
|
||||
None: 'None',
|
||||
DynamicRange: 'DynamicRange',
|
||||
PhysicalPartitionsOfTable: 'PhysicalPartitionsOfTable',
|
||||
};
|
||||
|
||||
export const PipelineStatusCodes = {
|
||||
// status codes: 'PreparingForCopy' | 'Copying' | 'CopyFinished' | 'RebuildingIndexes' | 'Succeeded' | 'Failed' | 'Canceled',
|
||||
PreparingForCopy: 'PreparingForCopy',
|
||||
Copying: 'Copying',
|
||||
CopyFinished: 'CopyFinished',
|
||||
RebuildingIndexes: 'RebuildingIndexes',
|
||||
Succeeded: 'Succeeded',
|
||||
Failed: 'Failed',
|
||||
Canceled: 'Canceled',
|
||||
|
||||
// legacy status codes
|
||||
Queued: 'Queued',
|
||||
InProgress: 'InProgress',
|
||||
Cancelled: 'Cancelled',
|
||||
};
|
||||
|
||||
const _dateFormatter = new Intl.DateTimeFormat(
|
||||
undefined, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
|
||||
const _numberFormatter = new Intl.NumberFormat(
|
||||
undefined, {
|
||||
style: 'decimal',
|
||||
useGrouping: true,
|
||||
minimumIntegerDigits: 1,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
|
||||
export function formatDateTimeString(dateTime: string): string {
|
||||
return dateTime
|
||||
? _dateFormatter.format(new Date(dateTime))
|
||||
: '';
|
||||
}
|
||||
|
||||
export function formatTime(miliseconds: number): string {
|
||||
if (miliseconds > 0) {
|
||||
// hh:mm:ss
|
||||
const matches = (new Date(miliseconds))?.toUTCString()?.match(/(\d\d:\d\d:\d\d)/) || [];
|
||||
return matches?.length > 0 ? matches[0] : '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function formatNumber(value: number): string {
|
||||
return value >= 0
|
||||
? _numberFormatter.format(value)
|
||||
: '';
|
||||
}
|
||||
|
||||
export function formatCopyThroughPut(value: number): string {
|
||||
return value >= 0
|
||||
? loc.sizeFormatter.format(value / 1024)
|
||||
: '';
|
||||
}
|
||||
|
||||
export function formatSizeBytes(sizeBytes: number): string {
|
||||
return formatSizeKb(sizeBytes / 1024);
|
||||
}
|
||||
|
||||
export function formatSizeKb(sizeKb: number): string {
|
||||
return loc.formatSizeMb(sizeKb / 1024);
|
||||
}
|
||||
|
||||
export function getMigrationTargetType(migration: DatabaseMigration | undefined): string {
|
||||
const id = migration?.id?.toLowerCase() || '';
|
||||
if (id?.indexOf(SQLTargetAssetType.SQLMI.toLowerCase()) > -1) {
|
||||
return loc.SQL_MANAGED_INSTANCE;
|
||||
}
|
||||
@@ -29,8 +105,8 @@ export function getMigrationTargetType(migration: DatabaseMigration): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
export function getMigrationTargetTypeEnum(migration: DatabaseMigration): MigrationTargetType | undefined {
|
||||
switch (migration.type) {
|
||||
export function getMigrationTargetTypeEnum(migration: DatabaseMigration | undefined): MigrationTargetType | undefined {
|
||||
switch (migration?.type) {
|
||||
case SQLTargetAssetType.SQLMI:
|
||||
return MigrationTargetType.SQLMI;
|
||||
case SQLTargetAssetType.SQLVM:
|
||||
@@ -42,37 +118,86 @@ export function getMigrationTargetTypeEnum(migration: DatabaseMigration): Migrat
|
||||
}
|
||||
}
|
||||
|
||||
export function getMigrationMode(migration: DatabaseMigration): string {
|
||||
export function getMigrationMode(migration: DatabaseMigration | undefined): string {
|
||||
return isOfflineMigation(migration)
|
||||
? loc.OFFLINE
|
||||
: loc.ONLINE;
|
||||
}
|
||||
|
||||
export function getMigrationModeEnum(migration: DatabaseMigration): MigrationMode {
|
||||
export function getMigrationModeEnum(migration: DatabaseMigration | undefined): MigrationMode {
|
||||
return isOfflineMigation(migration)
|
||||
? MigrationMode.OFFLINE
|
||||
: MigrationMode.ONLINE;
|
||||
}
|
||||
|
||||
export function isOfflineMigation(migration: DatabaseMigration): boolean {
|
||||
return migration.properties.offlineConfiguration?.offline === true;
|
||||
export function isOfflineMigation(migration: DatabaseMigration | undefined): boolean {
|
||||
return migration?.properties?.offlineConfiguration?.offline === true;
|
||||
}
|
||||
|
||||
export function isBlobMigration(migration: DatabaseMigration): boolean {
|
||||
export function isBlobMigration(migration: DatabaseMigration | undefined): boolean {
|
||||
return migration?.properties?.backupConfiguration?.sourceLocation?.fileStorageType === FileStorageType.AzureBlob;
|
||||
}
|
||||
|
||||
export function getMigrationStatus(migration: DatabaseMigration): string {
|
||||
return migration.properties.migrationStatus
|
||||
?? migration.properties.provisioningState;
|
||||
export function getMigrationStatus(migration: DatabaseMigration | undefined): string | undefined {
|
||||
return migration?.properties.migrationStatus
|
||||
?? migration?.properties.provisioningState;
|
||||
}
|
||||
|
||||
export function hasMigrationOperationId(migration: DatabaseMigration | undefined): boolean {
|
||||
const migrationId = migration?.id ?? '';
|
||||
const migationOperationId = migration?.properties?.migrationOperationId ?? '';
|
||||
return migrationId.length > 0
|
||||
&& migationOperationId.length > 0;
|
||||
}
|
||||
|
||||
export function canRetryMigration(status: string | undefined): boolean {
|
||||
return status === undefined ||
|
||||
status === MigrationStatus.Failed ||
|
||||
status === MigrationStatus.Succeeded ||
|
||||
status === MigrationStatus.Canceled;
|
||||
export function canCancelMigration(migration: DatabaseMigration | undefined): boolean {
|
||||
const status = getMigrationStatus(migration);
|
||||
return hasMigrationOperationId(migration)
|
||||
&& (status === MigrationStatus.InProgress ||
|
||||
status === MigrationStatus.Retriable ||
|
||||
status === MigrationStatus.Creating);
|
||||
}
|
||||
|
||||
export function canDeleteMigration(migration: DatabaseMigration | undefined): boolean {
|
||||
const status = getMigrationStatus(migration);
|
||||
return status === MigrationStatus.Canceled
|
||||
|| status === MigrationStatus.Failed
|
||||
|| status === MigrationStatus.Retriable
|
||||
|| status === MigrationStatus.Succeeded;
|
||||
}
|
||||
|
||||
export function canRetryMigration(migration: DatabaseMigration | undefined): boolean {
|
||||
const status = getMigrationStatus(migration);
|
||||
return status === MigrationStatus.Canceled
|
||||
|| status === MigrationStatus.Retriable
|
||||
|| status === MigrationStatus.Failed
|
||||
|| status === MigrationStatus.Succeeded;
|
||||
}
|
||||
|
||||
export function canCutoverMigration(migration: DatabaseMigration | undefined): boolean {
|
||||
const status = getMigrationStatus(migration);
|
||||
return hasMigrationOperationId(migration)
|
||||
&& status === MigrationStatus.InProgress
|
||||
&& isOnlineMigration(migration)
|
||||
&& isFullBackupRestored(migration);
|
||||
}
|
||||
|
||||
export function isActiveMigration(migration: DatabaseMigration | undefined): boolean {
|
||||
const status = getMigrationStatus(migration);
|
||||
return status === MigrationStatus.Completing
|
||||
|| status === MigrationStatus.Retriable
|
||||
|| status === MigrationStatus.Creating
|
||||
|| status === MigrationStatus.InProgress;
|
||||
}
|
||||
|
||||
export function isFullBackupRestored(migration: DatabaseMigration | undefined): boolean {
|
||||
const fileName = migration?.properties?.migrationStatusDetails?.lastRestoredFilename ?? '';
|
||||
return migration?.properties?.migrationStatusDetails?.isFullBackupRestored
|
||||
|| fileName.length > 0;
|
||||
}
|
||||
|
||||
export function isOnlineMigration(migration: DatabaseMigration | undefined): boolean {
|
||||
return getMigrationModeEnum(migration) === MigrationMode.ONLINE;
|
||||
}
|
||||
|
||||
export function selectDatabasesFromList(selectedDbs: string[], databaseTableValues: azdata.DeclarativeTableCellValue[][]): azdata.DeclarativeTableCellValue[][] {
|
||||
|
||||
@@ -45,6 +45,9 @@ export class IconPathHelper {
|
||||
public static stop: IconPath;
|
||||
public static view: IconPath;
|
||||
public static sqlMigrationService: IconPath;
|
||||
public static addNew: IconPath;
|
||||
public static breadCrumb: IconPath;
|
||||
public static allTables: IconPath;
|
||||
|
||||
public static setExtensionContext(context: vscode.ExtensionContext) {
|
||||
IconPathHelper.copy = {
|
||||
@@ -183,5 +186,17 @@ export class IconPathHelper {
|
||||
light: context.asAbsolutePath('images/sqlMigrationService.svg'),
|
||||
dark: context.asAbsolutePath('images/sqlMigrationService.svg'),
|
||||
};
|
||||
IconPathHelper.addNew = {
|
||||
light: context.asAbsolutePath('images/addNew.svg'),
|
||||
dark: context.asAbsolutePath('images/addNew.svg'),
|
||||
};
|
||||
IconPathHelper.breadCrumb = {
|
||||
light: context.asAbsolutePath('images/breadCrumb.svg'),
|
||||
dark: context.asAbsolutePath('images/breadCrumb.svg'),
|
||||
};
|
||||
IconPathHelper.allTables = {
|
||||
light: context.asAbsolutePath('images/allTables.svg'),
|
||||
dark: context.asAbsolutePath('images/allTables.svg'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as nls from 'vscode-nls';
|
||||
import { EOL } from 'os';
|
||||
import { MigrationStatus } from '../models/migrationLocalStorage';
|
||||
import { MigrationSourceAuthenticationType } from '../models/stateMachine';
|
||||
import { ParallelCopyTypeCodes, PipelineStatusCodes } from './helper';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
|
||||
@@ -403,6 +404,8 @@ export const IR_PAGE_TITLE = localize('sql.migration.ir.page.title', "Azure Data
|
||||
export const IR_PAGE_DESCRIPTION = localize('sql.migration.ir.page.description', "Azure Database Migration Service orchestrates database migration activities and tracks their progress. You can select an existing Database Migration Service as an Azure SQL target if you have created one previously, or create a new one below.");
|
||||
export const SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR = localize('sql.migration.ir.page.sql.migration.service.not.found', "No Database Migration Service found. Create a new one.");
|
||||
export const CREATE_NEW = localize('sql.migration.create.new', "Create new");
|
||||
export const CREATE_NEW_MIGRATION_SERVICE = localize('sql.migration.create.new.migration.service', "Create new migration service");
|
||||
export const CREATE_NEW_RESOURCE_GROUP = localize('sql.migration.create.new.resource.group', "Create new resource group");
|
||||
export const INVALID_SERVICE_ERROR = localize('sql.migration.invalid.migration.service.error', "Select a valid Database Migration Service.");
|
||||
export const SERVICE_OFFLINE_ERROR = localize('sql.migration.invalid.migration.service.offline.error', "Select a Database Migration Service that is connected to a node.");
|
||||
export const AUTHENTICATION_KEYS = localize('sql.migration.authentication.types', "Authentication keys");
|
||||
@@ -518,9 +521,9 @@ export const NOTEBOOK_SQL_MIGRATION_ASSESSMENT_TITLE = localize('sql.migration.s
|
||||
export const NOTEBOOK_OPEN_ERROR = localize('sql.migration.notebook.open.error', "Failed to open the migration notebook.");
|
||||
|
||||
// Dashboard
|
||||
export function DASHBOARD_REFRESH_MIGRATIONS(error: string): string {
|
||||
return localize('sql.migration.refresh.migrations.error', "An error occurred while refreshing the migrations list: '{0}'. Please check your linked Azure connection and click refresh to try again.", error);
|
||||
}
|
||||
export const DASHBOARD_REFRESH_MIGRATIONS_TITLE = localize('sql.migration.refresh.migrations.error.title', 'An error has occured while refreshing the migrations list.');
|
||||
export const DASHBOARD_REFRESH_MIGRATIONS_LABEL = localize('sql.migration.refresh.migrations.error.label', "An error occurred while refreshing the migrations list. Please check your linked Azure connection and click refresh to try again.");
|
||||
|
||||
export const DASHBOARD_TITLE = localize('sql.migration.dashboard.title', "Azure SQL Migration");
|
||||
export const DASHBOARD_DESCRIPTION = localize('sql.migration.dashboard.description', "Determine the migration readiness of your SQL Server instances, identify a recommended Azure SQL target, and complete the migration of your SQL Server instance to Azure SQL Managed Instance or SQL Server on Azure Virtual Machines.");
|
||||
export const DASHBOARD_MIGRATE_TASK_BUTTON_TITLE = localize('sql.migration.dashboard.migrate.task.button', "Migrate to Azure SQL");
|
||||
@@ -547,6 +550,7 @@ export function MIGRATION_INPROGRESS_WARNING(count: number) {
|
||||
export const FEEDBACK_ISSUE_TITLE = localize('sql.migration.feedback.issue.title', "Feedback on the migration experience");
|
||||
|
||||
//Migration cutover dialog
|
||||
export const BREADCRUMB_MIGRATIONS = localize('sql.migration.details.breadcrumb.migrations', 'Migrations');
|
||||
export const MIGRATION_CUTOVER = localize('sql.migration.cutover', "Migration cutover");
|
||||
export const COMPLETE_CUTOVER = localize('sql.migration.complete.cutover', "Complete cutover");
|
||||
export const SOURCE_DATABASE = localize('sql.migration.source.database', "Source database name");
|
||||
@@ -588,6 +592,16 @@ export const NA = localize('sql.migration.na', "N/A");
|
||||
export const EMPTY_TABLE_TEXT = localize('sql.migration.empty.table.text', "No backup files");
|
||||
export const EMPTY_TABLE_SUBTEXT = localize('sql.migration.empty.table.subtext', "If results were expected, verify the connection to the SQL Server instance.");
|
||||
export const MIGRATION_CUTOVER_ERROR = localize('sql.migration.cutover.error', 'An error occurred while initiating cutover.');
|
||||
export const REFRESH_BUTTON_TEXT = localize('sql.migration.details.refresh', 'Refresh');
|
||||
export const SERVER_OBJECTS_FIELD_LABEL = localize('sql.migration.details.serverobjects.field.label', 'Server objects');
|
||||
export const SERVER_OBJECTS_LABEL = localize('sql.migration.details.serverobjects.label', 'Server objects');
|
||||
export const SERVER_OBJECTS_ALL_TABLES_LABEL = localize('sql.migration.details.serverobjects.all.tables.label', 'Total tables');
|
||||
export const SERVER_OBJECTS_IN_PROGRESS_TABLES_LABEL = localize('sql.migration.details.serverobjects.inprogress.tables.label', 'In progress');
|
||||
export const SERVER_OBJECTS_SUCCESSFUL_TABLES_LABEL = localize('sql.migration.details.serverobjects.successful.tables.label', 'Successful');
|
||||
export const SERVER_OBJECTS_FAILED_TABLES_LABEL = localize('sql.migration.details.serverobjects.failed.tables.label', 'Failed');
|
||||
export const SERVER_OBJECTS_CANCELLED_TABLES_LABEL = localize('sql.migration.details.serverobjects.cancelled.tables.label', 'Cancelled');
|
||||
export const FILTER_SERVER_OBJECTS_PLACEHOLDER = localize('sql.migration.details.serverobjects.filter.label', 'Filter table migration results');
|
||||
export const FILTER_SERVER_OBJECTS_ARIA_LABEL = localize('sql.migration.details.serverobjects.filter.aria.label', 'Filter table migration results using keywords');
|
||||
|
||||
//Migration confirm cutover dialog
|
||||
export const COMPLETING_CUTOVER_WARNING = localize('sql.migration.completing.cutover.warning', "Completing cutover without restoring all the backups may result in a data loss.");
|
||||
@@ -616,6 +630,7 @@ export const MIGRATION_CANNOT_CUTOVER = localize('sql.migration.cannot.cutover',
|
||||
export const FILE_NAME = localize('sql.migration.file.name', "File name");
|
||||
export const SIZE_COLUMN_HEADER = localize('sql.migration.size.column.header', "Size");
|
||||
export const NO_PENDING_BACKUPS = localize('sql.migration.no.pending.backups', "No pending backups. Click refresh to check current status.");
|
||||
|
||||
//Migration status dialog
|
||||
export const ADD_ACCOUNT = localize('sql.migration.status.add.account', "Add account");
|
||||
export const ADD_ACCOUNT_MESSAGE = localize('sql.migration.status.add.account.MESSAGE', "Add your Azure account to view existing migrations and their status.");
|
||||
@@ -625,11 +640,14 @@ export const STATUS_ONGOING = localize('sql.migration.status.dropdown.ongoing',
|
||||
export const STATUS_COMPLETING = localize('sql.migration.status.dropdown.completing', "Status: Completing");
|
||||
export const STATUS_SUCCEEDED = localize('sql.migration.status.dropdown.succeeded', "Status: Succeeded");
|
||||
export const STATUS_FAILED = localize('sql.migration.status.dropdown.failed', "Status: Failed");
|
||||
export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migration', "Search for migrations");
|
||||
export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migration', "Filter migration results");
|
||||
export const ONLINE = localize('sql.migration.online', "Online");
|
||||
export const OFFLINE = localize('sql.migration.offline', "Offline");
|
||||
export const DATABASE = localize('sql.migration.database', "Database");
|
||||
export const STATUS_COLUMN = localize('sql.migration.database.status.column', "Status");
|
||||
export const SRC_DATABASE = localize('sql.migration.src.database', "Source database");
|
||||
export const SRC_SERVER = localize('sql.migration.src.server', "Source name");
|
||||
|
||||
export const STATUS_COLUMN = localize('sql.migration.database.status.column', "Migration status");
|
||||
export const DATABASE_MIGRATION_SERVICE = localize('sql.migration.database.migration.service', "Database Migration Service");
|
||||
export const DURATION = localize('sql.migration.duration', "Duration");
|
||||
export const AZURE_SQL_TARGET = localize('sql.migration.azure.sql.target', "Target type");
|
||||
@@ -637,7 +655,9 @@ export const SQL_MANAGED_INSTANCE = localize('sql.migration.sql.managed.instance
|
||||
export const SQL_VIRTUAL_MACHINE = localize('sql.migration.sql.virtual.machine', "SQL Virtual Machine");
|
||||
export const SQL_DATABASE = localize('sql.migration.sql.database', "SQL Database");
|
||||
export const TARGET_AZURE_SQL_INSTANCE_NAME = localize('sql.migration.target.azure.sql.instance.name', "Target name");
|
||||
export const MIGRATION_MODE = localize('sql.migration.cutover.type', "Migration mode");
|
||||
export const TARGET_SERVER_COLUMN = localize('sql.migration.target.azure.sql.instance.server.name', "Target name");
|
||||
export const TARGET_DATABASE_COLUMN = localize('sql.migration.target.azure.sql.instance.database.name', "Target database");
|
||||
export const MIGRATION_MODE = localize('sql.migration.cutover.type', "Mode");
|
||||
export const START_TIME = localize('sql.migration.start.time', "Start time");
|
||||
export const FINISH_TIME = localize('sql.migration.finish.time', "Finish time");
|
||||
|
||||
@@ -648,20 +668,53 @@ export function STATUS_VALUE(status: string, count: number): string {
|
||||
return localize('sql.migration.status.error.count.none', "{0}", StatusLookup[status] ?? status);
|
||||
}
|
||||
|
||||
export const MIGRATION_ERROR_DETAILS_TITLE = localize('sql.migration.error.details.title', "Migration error details");
|
||||
export const MIGRATION_ERROR_DETAILS_LABEL = localize('sql.migration.error.details.label', "Migration error(s))");
|
||||
export const OPEN_MIGRATION_DETAILS_ERROR = localize('sql.migration.open.migration.destails.error', "Error opening migration details dialog");
|
||||
export const OPEN_MIGRATION_TARGET_ERROR = localize('sql.migration.open.migration.target.error', "Error opening migration target");
|
||||
export const OPEN_MIGRATION_SERVICE_ERROR = localize('sql.migration.open.migration.service.error', "Error opening migration service dialog");
|
||||
export const LOAD_MIGRATION_LIST_ERROR = localize('sql.migration.load.migration.list.error', "Error loading migrations list");
|
||||
export const ERROR_DIALOG_CLEAR_BUTTON_LABEL = localize('sql.migration.error.dialog.clear.button.label', "Clear");
|
||||
|
||||
export interface LookupTable<T> {
|
||||
[key: string]: T;
|
||||
}
|
||||
|
||||
|
||||
export const StatusLookup: LookupTable<string | undefined> = {
|
||||
['InProgress']: localize('sql.migration.status.inprogress', 'In progress'),
|
||||
['Succeeded']: localize('sql.migration.status.succeeded', 'Succeeded'),
|
||||
['Creating']: localize('sql.migration.status.creating', 'Creating'),
|
||||
['Completing']: localize('sql.migration.status.completing', 'Completing'),
|
||||
['Canceling']: localize('sql.migration.status.canceling', 'Canceling'),
|
||||
['Failed']: localize('sql.migration.status.failed', 'Failed'),
|
||||
[MigrationStatus.InProgress]: localize('sql.migration.status.inprogress', 'In progress'),
|
||||
[MigrationStatus.Succeeded]: localize('sql.migration.status.succeeded', 'Succeeded'),
|
||||
[MigrationStatus.Creating]: localize('sql.migration.status.creating', 'Creating'),
|
||||
[MigrationStatus.Completing]: localize('sql.migration.status.completing', 'Completing'),
|
||||
[MigrationStatus.Retriable]: localize('sql.migration.status.retriable', 'Retriable'),
|
||||
[MigrationStatus.Canceling]: localize('sql.migration.status.canceling', 'Canceling'),
|
||||
[MigrationStatus.Canceled]: localize('sql.migration.status.canceled', 'Canceled'),
|
||||
[MigrationStatus.Failed]: localize('sql.migration.status.failed', 'Failed'),
|
||||
default: undefined
|
||||
};
|
||||
|
||||
export const PipelineRunStatus: LookupTable<string | undefined> = {
|
||||
// status codes: ['PreparingForCopy' | 'Copying' | 'CopyFinished' | 'RebuildingIndexes' | 'Succeeded' | 'Failed' | 'Canceled']
|
||||
[PipelineStatusCodes.PreparingForCopy]: localize('sql.migration.copy.status.preparingforcopy', 'Preparing'),
|
||||
[PipelineStatusCodes.Copying]: localize('sql.migration.copy.status.copying', 'Copying'),
|
||||
[PipelineStatusCodes.CopyFinished]: localize('sql.migration.copy.status.copyfinished', 'Copy finished'),
|
||||
[PipelineStatusCodes.RebuildingIndexes]: localize('sql.migration.copy.status.rebuildingindexes', 'Rebuilding indexes'),
|
||||
[PipelineStatusCodes.Succeeded]: localize('sql.migration.copy.status.succeeded', 'Succeeded'),
|
||||
[PipelineStatusCodes.Failed]: localize('sql.migration.copy.status.failed', 'Failed'),
|
||||
[PipelineStatusCodes.Canceled]: localize('sql.migration.copy.status.canceled', 'Canceled'),
|
||||
|
||||
// legacy status codes ['Queued', 'InProgress', 'Cancelled']
|
||||
[PipelineStatusCodes.Queued]: localize('sql.migration.copy.status.queued', 'Queued'),
|
||||
[PipelineStatusCodes.InProgress]: localize('sql.migration.copy.status.inprogress', 'In progress'),
|
||||
[PipelineStatusCodes.Cancelled]: localize('sql.migration.copy.status.cancelled', 'Cancelled'),
|
||||
};
|
||||
|
||||
export const ParallelCopyType: LookupTable<string | undefined> = {
|
||||
[ParallelCopyTypeCodes.None]: localize('sql.migration.parallel.copy.type.none', 'None'),
|
||||
[ParallelCopyTypeCodes.PhysicalPartitionsOfTable]: localize('sql.migration.parallel.copy.type.physical', 'Physical partitions'),
|
||||
[ParallelCopyTypeCodes.DynamicRange]: localize('sql.migration.parallel.copy.type.dynamic', 'Dynamic range'),
|
||||
};
|
||||
|
||||
export function STATUS_WARNING_COUNT(status: string, count: number): string | undefined {
|
||||
if (status === MigrationStatus.InProgress ||
|
||||
status === MigrationStatus.Creating ||
|
||||
@@ -699,6 +752,27 @@ export function SEC(sec: number): string {
|
||||
return localize('sql.migration.sec', "{0} sec", sec);
|
||||
}
|
||||
|
||||
export const sizeFormatter = new Intl.NumberFormat(
|
||||
undefined, {
|
||||
style: 'decimal',
|
||||
useGrouping: true,
|
||||
minimumIntegerDigits: 1,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
export function formatSizeMb(sizeMb: number): string {
|
||||
if (isNaN(sizeMb) || sizeMb < 0) {
|
||||
return '';
|
||||
} else if (sizeMb < 1024) {
|
||||
return localize('sql.migration.size.mb', "{0} MB", sizeFormatter.format(sizeMb));
|
||||
} else if (sizeMb < 1024 * 1024) {
|
||||
return localize('sql.migration.size.gb', "{0} GB", sizeFormatter.format(sizeMb / 1024));
|
||||
} else {
|
||||
return localize('sql.migration.size.tb', "{0} TB", sizeFormatter.format(sizeMb / 1024 / 1024));
|
||||
}
|
||||
}
|
||||
|
||||
// SQL Migration Service Details page.
|
||||
export const SQL_MIGRATION_SERVICE_DETAILS_SUB_TITLE = localize('sql.migration.service.details.dialog.title', "Azure Database Migration Service");
|
||||
export const SQL_MIGRATION_SERVICE_DETAILS_BUTTON_LABEL = localize('sql.migration.service.details.button.label', "Close");
|
||||
@@ -761,6 +835,9 @@ export function WARNINGS_COUNT(totalCount: number): string {
|
||||
export const AUTHENTICATION_TYPE = localize('sql.migration.authentication.type', "Authentication type");
|
||||
|
||||
export const REFRESH_BUTTON_LABEL = localize('sql.migration.status.refresh.label', 'Refresh');
|
||||
export const STATUS_LABEL = localize('sql.migration.status.status.label', 'Status');
|
||||
export const SORT_LABEL = localize('sql.migration.migration.list.sort.label', 'Sort');
|
||||
export const ASCENDING_LABEL = localize('sql.migration.migration.list.ascending.label', 'Ascending');
|
||||
|
||||
// Saved Assessment Dialog
|
||||
export const NEXT_LABEL = localize('sql.migration.saved.assessment.next', "Next");
|
||||
@@ -786,3 +863,46 @@ export function MIGRATION_SERVICE_SERVICE_PROMPT(serviceName: string): string {
|
||||
return localize('sql.migration.service.prompt', '{0} (change)', serviceName);
|
||||
}
|
||||
export const MIGRATION_SERVICE_DESCRIPTION = localize('sql.migration.select.service.description', 'Azure Database Migration Service');
|
||||
|
||||
// Desktop tabs
|
||||
export const DESKTOP_MIGRATION_BUTTON_LABEL = localize('sql.migration.tab.button.migration.label', 'New migration');
|
||||
export const DESKTOP_MIGRATION_BUTTON_DESCRIPTION = localize('sql.migration.tab.button.migration.description', 'Migrate to Azure SQL');
|
||||
export const DESKTOP_SUPPORT_BUTTON_LABEL = localize('sql.migration.tab.button.support.label', 'New support request');
|
||||
export const DESKTOP_SUPPORT_BUTTON_DESCRIPTION = localize('sql.migration.tab.button.support.description', 'New support request');
|
||||
export const DESKTOP_FEEDBACK_BUTTON_LABEL = localize('sql.migration.tab.button.feedback.label', 'Feedback');
|
||||
export const DESKTOP_FEEDBACK_BUTTON_DESCRIPTION = localize('sql.migration.tab.button.feedback.description', 'Feedback');
|
||||
export const DESKTOP_DASHBOARD_TAB_TITLE = localize('sql.migration.tab.dashboard.title', 'Dashboard');
|
||||
export const DESKTOP_MIGRATIONS_TAB_TITLE = localize('sql.migration.tab.migrations.title', 'Migrations');
|
||||
|
||||
// dashboard tab
|
||||
export const DASHBOARD_HELP_LINK_MIGRATE_USING_ADS = localize('sql.migration.dashboard.help.link.migrateUsingADS', 'Migrate databases using Azure Data Studio');
|
||||
export const DASHBOARD_HELP_DESCRIPTION_MIGRATE_USING_ADS = localize('sql.migration.dashboard.help.description.migrateUsingADS', 'The Azure SQL Migration extension for Azure Data Studio provides capabilities to assess, get right-sized Azure recommendations and migrate SQL Server databases to Azure.');
|
||||
export const DASHBOARD_HELP_LINK_MI_TUTORIAL = localize('sql.migration.dashboard.help.link.mi', 'Tutorial: Migrate to Azure SQL Managed Instance (online)');
|
||||
export const DASHBOARD_HELP_DESCRIPTION_MI_TUTORIAL = localize('sql.migration.dashboard.help.description.mi', 'A step-by-step tutorial to migrate databases from a SQL Server instance (on-premises or Azure Virtual Machines) to Azure SQL Managed Instance with minimal downtime.');
|
||||
export const DASHBOARD_HELP_LINK_VM_TUTORIAL = localize('sql.migration.dashboard.help.link.vm', 'Tutorial: Migrate to SQL Server on Azure Virtual Machines (online)');
|
||||
export const DASHBOARD_HELP_DESCRIPTION_VMTUTORIAL = localize('sql.migration.dashboard.help.description.vm', 'A step-by-step tutorial to migrate databases from a SQL Server instance (on-premises) to SQL Server on Azure Virtual Machines with minimal downtime.');
|
||||
export const DASHBOARD_HELP_LINK_DMS_GUIDE = localize('sql.migration.dashboard.help.link.dmsGuide', 'Azure Database Migration Guides');
|
||||
export const DASHBOARD_HELP_DESCRIPTION_DMS_GUIDE = localize('sql.migration.dashboard.help.description.dmsGuide', 'A hub of migration articles that provides step-by-step guidance for migrating and modernizing your data assets in Azure.');
|
||||
|
||||
// Error info
|
||||
export const DATABASE_MIGRATION_STATUS_TITLE = localize('sql.migration.error.title', 'Migration status details');
|
||||
export const TABLE_MIGRATION_STATUS_TITLE = localize('sql.migration.table.error.title', 'Table migration status details');
|
||||
|
||||
export function DATABASE_MIGRATION_STATUS_LABEL(status?: string): string {
|
||||
return localize('sql.migration.database.migration.status.label', 'Database migration status: {0}', status ?? '');
|
||||
}
|
||||
|
||||
export function TABLE_MIGRATION_STATUS_LABEL(status?: string): string {
|
||||
return localize('sql.migration.table.migration.status.label', 'Table migration status: {0}', status ?? '');
|
||||
}
|
||||
|
||||
export const SQLDB_COL_TABLE_NAME = localize('sql.migration.sqldb.column.tablename', 'Table name');
|
||||
export const SQLDB_COL_DATA_READ = localize('sql.migration.sqldb.column.dataread', 'Data read');
|
||||
export const SQLDB_COL_DATA_WRITTEN = localize('sql.migration.sqldb.column.datawritten', 'Data written');
|
||||
export const SQLDB_COL_ROWS_READ = localize('sql.migration.sqldb.column.rowsread', 'Rows read');
|
||||
export const SQLDB_COL_ROWS_COPIED = localize('sql.migration.sqldb.column.rowscopied', 'Rows copied');
|
||||
export const SQLDB_COL_COPY_THROUGHPUT = localize('sql.migration.sqldb.column.copythroughput', 'Copy throughput');
|
||||
export const SQLDB_COL_COPY_DURATION = localize('sql.migration.sqldb.column.copyduration', 'Copy duration');
|
||||
export const SQLDB_COL_PARRALEL_COPY_TYPE = localize('sql.migration.sqldb.column.parallelcopytype', 'Parallel copy type');
|
||||
export const SQLDB_COL_USED_PARALLEL_COPIES = localize('sql.migration.sqldb.column.usedparallelcopies', 'Used parallel copies');
|
||||
export const SQLDB_COL_COPY_START = localize('sql.migration.sqldb.column.copystart', 'Copy start');
|
||||
|
||||
789
extensions/sql-migration/src/dashboard/dashboardTab.ts
Normal file
789
extensions/sql-migration/src/dashboard/dashboardTab.ts
Normal file
@@ -0,0 +1,789 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
|
||||
import * as styles from '../constants/styles';
|
||||
import * as loc from '../constants/strings';
|
||||
import { filterMigrations } from '../api/utils';
|
||||
import { DatabaseMigration } from '../api/azure';
|
||||
import { getCurrentMigrations, getSelectedServiceStatus, isServiceContextValid, MigrationLocalStorage } from '../models/migrationLocalStorage';
|
||||
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import { AdsMigrationStatus, MenuCommands, TabBase } from './tabBase';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
|
||||
interface IActionMetadata {
|
||||
title?: string,
|
||||
description?: string,
|
||||
link?: string,
|
||||
iconPath?: azdata.ThemedIconPath,
|
||||
command?: string;
|
||||
}
|
||||
|
||||
interface StatusCard {
|
||||
container: azdata.DivContainer;
|
||||
count: azdata.TextComponent,
|
||||
textContainer?: azdata.FlexContainer,
|
||||
warningContainer?: azdata.FlexContainer,
|
||||
warningText?: azdata.TextComponent,
|
||||
}
|
||||
|
||||
export const DashboardTabId = 'DashboardTab';
|
||||
|
||||
const maxWidth = 800;
|
||||
const BUTTON_CSS = {
|
||||
'font-size': '13px',
|
||||
'line-height': '18px',
|
||||
'margin': '4px 0',
|
||||
'text-align': 'left',
|
||||
};
|
||||
|
||||
export class DashboardTab extends TabBase<DashboardTab> {
|
||||
private _migrationStatusCardsContainer!: azdata.FlexContainer;
|
||||
private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent;
|
||||
private _inProgressMigrationButton!: StatusCard;
|
||||
private _inProgressWarningMigrationButton!: StatusCard;
|
||||
private _allMigrationButton!: StatusCard;
|
||||
private _successfulMigrationButton!: StatusCard;
|
||||
private _failedMigrationButton!: StatusCard;
|
||||
private _completingMigrationButton!: StatusCard;
|
||||
private _selectServiceText!: azdata.TextComponent;
|
||||
private _serviceContextButton!: azdata.ButtonComponent;
|
||||
private _refreshButton!: azdata.ButtonComponent;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.title = loc.DESKTOP_DASHBOARD_TAB_TITLE;
|
||||
this.id = DashboardTabId;
|
||||
this.icon = IconPathHelper.sqlMigrationLogo;
|
||||
}
|
||||
|
||||
public onDialogClosed = async (): Promise<void> =>
|
||||
await this.updateServiceContext(this._serviceContextButton);
|
||||
|
||||
public async create(
|
||||
view: azdata.ModelView,
|
||||
openMigrationsFcn: (status: AdsMigrationStatus) => Promise<void>,
|
||||
statusBar: DashboardStatusBar): Promise<DashboardTab> {
|
||||
|
||||
this.view = view;
|
||||
this.openMigrationFcn = openMigrationsFcn;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize(this.view);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
if (this.isRefreshing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this._migrationStatusCardLoadingContainer.loading = true;
|
||||
let migrations: DatabaseMigration[] = [];
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
migrations = await getCurrentMigrations();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.DASHBOARD_REFRESH_MIGRATIONS_TITLE,
|
||||
loc.DASHBOARD_REFRESH_MIGRATIONS_LABEL,
|
||||
e.message);
|
||||
logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', e);
|
||||
}
|
||||
|
||||
const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
|
||||
let warningCount = 0;
|
||||
for (let i = 0; i < inProgressMigrations.length; i++) {
|
||||
if (inProgressMigrations[i].properties.migrationFailureError?.message ||
|
||||
inProgressMigrations[i].properties.migrationStatusDetails?.fileUploadBlockingErrors ||
|
||||
inProgressMigrations[i].properties.migrationStatusDetails?.restoreBlockingReason) {
|
||||
warningCount += 1;
|
||||
}
|
||||
}
|
||||
if (warningCount > 0) {
|
||||
this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount);
|
||||
this._inProgressMigrationButton.container.display = 'none';
|
||||
this._inProgressWarningMigrationButton.container.display = '';
|
||||
} else {
|
||||
this._inProgressMigrationButton.container.display = '';
|
||||
this._inProgressWarningMigrationButton.container.display = 'none';
|
||||
}
|
||||
|
||||
this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString();
|
||||
this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString();
|
||||
|
||||
this._updateStatusCard(migrations, this._successfulMigrationButton, AdsMigrationStatus.SUCCEEDED, true);
|
||||
this._updateStatusCard(migrations, this._failedMigrationButton, AdsMigrationStatus.FAILED);
|
||||
this._updateStatusCard(migrations, this._completingMigrationButton, AdsMigrationStatus.COMPLETING);
|
||||
this._updateStatusCard(migrations, this._allMigrationButton, AdsMigrationStatus.ALL, true);
|
||||
|
||||
await this._updateSummaryStatus();
|
||||
this.isRefreshing = false;
|
||||
this._migrationStatusCardLoadingContainer.loading = false;
|
||||
}
|
||||
|
||||
protected async initialize(view: azdata.ModelView): Promise<void> {
|
||||
const container = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}).component();
|
||||
|
||||
const toolbar = view.modelBuilder.toolbarContainer();
|
||||
toolbar.addToolbarItems([
|
||||
<azdata.ToolbarComponent>{ component: this.createNewMigrationButton() },
|
||||
<azdata.ToolbarComponent>{ component: this.createNewSupportRequestButton() },
|
||||
<azdata.ToolbarComponent>{ component: this.createFeedbackButton() },
|
||||
]);
|
||||
|
||||
container.addItem(
|
||||
toolbar.component(),
|
||||
{ CSSStyles: { 'flex': '0 0 auto' } });
|
||||
|
||||
const header = this._createHeader(view);
|
||||
// Files need to have the vscode-file scheme to be loaded by ADS
|
||||
const watermarkUri = vscode.Uri
|
||||
.file(<string>IconPathHelper.migrationDashboardHeaderBackground.light)
|
||||
.with({ scheme: 'vscode-file' });
|
||||
|
||||
container.addItem(header, {
|
||||
CSSStyles: {
|
||||
'background-image': `
|
||||
url(${watermarkUri}),
|
||||
linear-gradient(0deg, rgba(0, 0, 0, 0.05) 0%, rgba(0, 0, 0, 0) 100%)`,
|
||||
'background-repeat': 'no-repeat',
|
||||
'background-position': '91.06% 100%',
|
||||
'margin-bottom': '20px'
|
||||
}
|
||||
});
|
||||
|
||||
const tasksContainer = await this._createTasks(view);
|
||||
header.addItem(tasksContainer, {
|
||||
CSSStyles: {
|
||||
'width': `${maxWidth}px`,
|
||||
'margin': '24px'
|
||||
}
|
||||
});
|
||||
container.addItem(
|
||||
await this._createFooter(view),
|
||||
{ CSSStyles: { 'margin': '0 24px' } });
|
||||
|
||||
this.content = container;
|
||||
}
|
||||
|
||||
private _createHeader(view: azdata.ModelView): azdata.FlexContainer {
|
||||
const header = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: maxWidth,
|
||||
}).component();
|
||||
const titleComponent = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.DASHBOARD_TITLE,
|
||||
width: '750px',
|
||||
CSSStyles: { ...styles.DASHBOARD_TITLE_CSS }
|
||||
}).component();
|
||||
|
||||
const descriptionComponent = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.DASHBOARD_DESCRIPTION,
|
||||
CSSStyles: { ...styles.NOTE_CSS }
|
||||
}).component();
|
||||
header.addItems([titleComponent, descriptionComponent], {
|
||||
CSSStyles: {
|
||||
'width': `${maxWidth}px`,
|
||||
'padding-left': '24px'
|
||||
}
|
||||
});
|
||||
return header;
|
||||
}
|
||||
|
||||
private async _createTasks(view: azdata.ModelView): Promise<azdata.Component> {
|
||||
const tasksContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'row',
|
||||
width: '100%',
|
||||
}).component();
|
||||
|
||||
const migrateButtonMetadata: IActionMetadata = {
|
||||
title: loc.DASHBOARD_MIGRATE_TASK_BUTTON_TITLE,
|
||||
description: loc.DASHBOARD_MIGRATE_TASK_BUTTON_DESCRIPTION,
|
||||
iconPath: IconPathHelper.sqlMigrationLogo,
|
||||
command: MenuCommands.StartMigration
|
||||
};
|
||||
|
||||
const preRequisiteListTitle = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.PRE_REQ_TITLE,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const migrateButton = this._createTaskButton(view, migrateButtonMetadata);
|
||||
|
||||
const preRequisiteListElement = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: [
|
||||
loc.PRE_REQ_1,
|
||||
loc.PRE_REQ_2,
|
||||
loc.PRE_REQ_3
|
||||
],
|
||||
CSSStyles: {
|
||||
...styles.SMALL_NOTE_CSS,
|
||||
'padding-left': '12px',
|
||||
'margin': '-0.5em 0px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const preRequisiteLearnMoreLink = view.modelBuilder.hyperlink()
|
||||
.withProps({
|
||||
label: loc.LEARN_MORE,
|
||||
ariaLabel: loc.LEARN_MORE_ABOUT_PRE_REQS,
|
||||
url: 'https://aka.ms/azuresqlmigrationextension',
|
||||
}).component();
|
||||
|
||||
const preReqContainer = view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
preRequisiteListTitle,
|
||||
preRequisiteListElement,
|
||||
preRequisiteLearnMoreLink])
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
|
||||
tasksContainer.addItem(migrateButton, {});
|
||||
tasksContainer.addItems(
|
||||
[preReqContainer],
|
||||
{ CSSStyles: { 'margin-left': '20px' } });
|
||||
|
||||
return tasksContainer;
|
||||
}
|
||||
|
||||
private _createTaskButton(view: azdata.ModelView, taskMetaData: IActionMetadata): azdata.Component {
|
||||
const maxHeight: number = 84;
|
||||
const maxWidth: number = 236;
|
||||
const buttonContainer = view.modelBuilder.button().withProps({
|
||||
buttonType: azdata.ButtonType.Informational,
|
||||
description: taskMetaData.description,
|
||||
height: maxHeight,
|
||||
iconHeight: 32,
|
||||
iconPath: taskMetaData.iconPath,
|
||||
iconWidth: 32,
|
||||
label: taskMetaData.title,
|
||||
title: taskMetaData.title,
|
||||
width: maxWidth,
|
||||
CSSStyles: {
|
||||
'border': '1px solid',
|
||||
'display': 'flex',
|
||||
'flex-direction': 'column',
|
||||
'justify-content': 'flex-start',
|
||||
'border-radius': '4px',
|
||||
'transition': 'all .5s ease',
|
||||
}
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
buttonContainer.onDidClick(async () => {
|
||||
if (taskMetaData.command) {
|
||||
await vscode.commands.executeCommand(taskMetaData.command);
|
||||
}
|
||||
}));
|
||||
return view.modelBuilder.divContainer().withItems([buttonContainer]).component();
|
||||
}
|
||||
|
||||
private async _createFooter(view: azdata.ModelView): Promise<azdata.Component> {
|
||||
const footerContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'row',
|
||||
width: maxWidth,
|
||||
justifyContent: 'flex-start'
|
||||
}).component();
|
||||
const statusContainer = await this._createMigrationStatusContainer(view);
|
||||
const videoLinksContainer = this._createVideoLinks(view);
|
||||
footerContainer.addItem(statusContainer);
|
||||
footerContainer.addItem(
|
||||
videoLinksContainer,
|
||||
{ CSSStyles: { 'padding-left': '8px', } });
|
||||
|
||||
return footerContainer;
|
||||
}
|
||||
|
||||
private _createVideoLinks(view: azdata.ModelView): azdata.Component {
|
||||
const linksContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: '440px',
|
||||
height: '365px',
|
||||
justifyContent: 'flex-start',
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'border': '1px solid rgba(0, 0, 0, 0.1)',
|
||||
'padding': '10px',
|
||||
'overflow': 'scroll',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const titleComponent = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.HELP_TITLE,
|
||||
CSSStyles: { ...styles.SECTION_HEADER_CSS }
|
||||
})
|
||||
.component();
|
||||
|
||||
linksContainer.addItems(
|
||||
[titleComponent],
|
||||
{ CSSStyles: { 'margin-bottom': '16px' } });
|
||||
|
||||
const links = [
|
||||
{
|
||||
title: loc.DASHBOARD_HELP_LINK_MIGRATE_USING_ADS,
|
||||
description: loc.DASHBOARD_HELP_DESCRIPTION_MIGRATE_USING_ADS,
|
||||
link: 'https://docs.microsoft.com/azure/dms/migration-using-azure-data-studio'
|
||||
},
|
||||
{
|
||||
title: loc.DASHBOARD_HELP_LINK_MI_TUTORIAL,
|
||||
description: loc.DASHBOARD_HELP_DESCRIPTION_MI_TUTORIAL,
|
||||
link: 'https://docs.microsoft.com/azure/dms/tutorial-sql-server-managed-instance-online-ads'
|
||||
},
|
||||
{
|
||||
title: loc.DASHBOARD_HELP_LINK_VM_TUTORIAL,
|
||||
description: loc.DASHBOARD_HELP_DESCRIPTION_VMTUTORIAL,
|
||||
link: 'https://docs.microsoft.com/azure/dms/tutorial-sql-server-to-virtual-machine-online-ads'
|
||||
},
|
||||
{
|
||||
title: loc.DASHBOARD_HELP_LINK_DMS_GUIDE,
|
||||
description: loc.DASHBOARD_HELP_DESCRIPTION_DMS_GUIDE,
|
||||
link: 'https://docs.microsoft.com/data-migration/'
|
||||
},
|
||||
];
|
||||
|
||||
linksContainer.addItems(links.map(l => this._createLink(view, l)), {});
|
||||
|
||||
const videoLinks: IActionMetadata[] = [];
|
||||
const videosContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'row',
|
||||
width: maxWidth,
|
||||
}).component();
|
||||
videosContainer.addItems(videoLinks.map(l => this._createVideoLink(view, l)), {});
|
||||
linksContainer.addItem(videosContainer);
|
||||
|
||||
return linksContainer;
|
||||
}
|
||||
|
||||
private _createLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
|
||||
const maxWidth = 400;
|
||||
const labelsContainer = view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'column',
|
||||
'width': `${maxWidth}px`,
|
||||
'justify-content': 'flex-start',
|
||||
'margin-bottom': '12px'
|
||||
}
|
||||
}).component();
|
||||
const linkContainer = view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'row',
|
||||
'width': `${maxWidth}px`,
|
||||
'justify-content': 'flex-start',
|
||||
'margin-bottom': '4px'
|
||||
}
|
||||
|
||||
}).component();
|
||||
const descriptionComponent = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: linkMetaData.description,
|
||||
width: maxWidth,
|
||||
CSSStyles: { ...styles.NOTE_CSS }
|
||||
}).component();
|
||||
const linkComponent = view.modelBuilder.hyperlink()
|
||||
.withProps({
|
||||
label: linkMetaData.title!,
|
||||
url: linkMetaData.link!,
|
||||
showLinkIcon: true,
|
||||
CSSStyles: { ...styles.BODY_CSS }
|
||||
}).component();
|
||||
linkContainer.addItem(linkComponent);
|
||||
labelsContainer.addItems([linkContainer, descriptionComponent]);
|
||||
return labelsContainer;
|
||||
}
|
||||
|
||||
private _createVideoLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
|
||||
const maxWidth = 150;
|
||||
const videosContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: maxWidth,
|
||||
justifyContent: 'flex-start'
|
||||
}).component();
|
||||
const video1Container = view.modelBuilder.divContainer()
|
||||
.withProps({
|
||||
clickable: true,
|
||||
width: maxWidth,
|
||||
height: '100px'
|
||||
}).component();
|
||||
const descriptionComponent = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: linkMetaData.description,
|
||||
width: maxWidth,
|
||||
height: '50px',
|
||||
CSSStyles: { ...styles.BODY_CSS }
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
video1Container.onDidClick(async () => {
|
||||
if (linkMetaData.link) {
|
||||
await vscode.env.openExternal(vscode.Uri.parse(linkMetaData.link));
|
||||
}
|
||||
}));
|
||||
videosContainer.addItem(video1Container, {
|
||||
CSSStyles: {
|
||||
'background-image': `url(${vscode.Uri.file(<string>linkMetaData.iconPath?.light)})`,
|
||||
'background-repeat': 'no-repeat',
|
||||
'background-position': 'top',
|
||||
'width': `${maxWidth}px`,
|
||||
'height': '104px',
|
||||
'background-size': `${maxWidth}px 120px`
|
||||
}
|
||||
});
|
||||
videosContainer.addItem(descriptionComponent);
|
||||
return videosContainer;
|
||||
}
|
||||
|
||||
private _createStatusCard(
|
||||
view: azdata.ModelView,
|
||||
cardIconPath: IconPath,
|
||||
cardTitle: string,
|
||||
hasSubtext: boolean = false
|
||||
): StatusCard {
|
||||
const buttonWidth = '400px';
|
||||
const buttonHeight = hasSubtext ? '70px' : '50px';
|
||||
const statusCard = view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'width': buttonWidth,
|
||||
'height': buttonHeight,
|
||||
'align-items': 'center',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const statusIcon = view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: cardIconPath!.light,
|
||||
iconHeight: 24,
|
||||
iconWidth: 24,
|
||||
height: 32,
|
||||
CSSStyles: { 'margin': '0 8px' }
|
||||
}).component();
|
||||
|
||||
const textContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
|
||||
const cardTitleText = view.modelBuilder.text()
|
||||
.withProps({ value: cardTitle })
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
'width': '240px',
|
||||
}
|
||||
}).component();
|
||||
textContainer.addItem(cardTitleText);
|
||||
|
||||
const cardCount = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: '0',
|
||||
CSSStyles: {
|
||||
...styles.BIG_NUMBER_CSS,
|
||||
'margin': '0 0 0 8px',
|
||||
'text-align': 'center',
|
||||
}
|
||||
}).component();
|
||||
|
||||
let warningContainer;
|
||||
let warningText;
|
||||
if (hasSubtext) {
|
||||
const warningIcon = view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.warning,
|
||||
iconWidth: 12,
|
||||
iconHeight: 12,
|
||||
width: 12,
|
||||
height: 18,
|
||||
}).component();
|
||||
|
||||
const warningDescription = '';
|
||||
warningText = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: warningDescription,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'padding-left': '8px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
warningContainer = view.modelBuilder.flexContainer()
|
||||
.withItems(
|
||||
[warningIcon, warningText],
|
||||
{ flex: '0 0 auto' })
|
||||
.withProps({ CSSStyles: { 'align-items': 'center' } })
|
||||
.component();
|
||||
|
||||
textContainer.addItem(warningContainer);
|
||||
}
|
||||
|
||||
statusCard.addItems([
|
||||
statusIcon,
|
||||
textContainer,
|
||||
cardCount,
|
||||
]);
|
||||
|
||||
const compositeButton = view.modelBuilder.divContainer()
|
||||
.withItems([statusCard])
|
||||
.withProps({
|
||||
ariaRole: 'button',
|
||||
ariaLabel: loc.SHOW_STATUS,
|
||||
clickable: true,
|
||||
CSSStyles: {
|
||||
'height': buttonHeight,
|
||||
'margin-bottom': '16px',
|
||||
'border': '1px solid',
|
||||
'display': 'flex',
|
||||
'flex-direction': 'column',
|
||||
'justify-content': 'flex-start',
|
||||
'border-radius': '4px',
|
||||
'transition': 'all .5s ease',
|
||||
}
|
||||
}).component();
|
||||
return {
|
||||
container: compositeButton,
|
||||
count: cardCount,
|
||||
textContainer: textContainer,
|
||||
warningContainer: warningContainer,
|
||||
warningText: warningText
|
||||
};
|
||||
}
|
||||
|
||||
private async _createMigrationStatusContainer(view: azdata.ModelView): Promise<azdata.FlexContainer> {
|
||||
const statusContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: '400px',
|
||||
height: '365px',
|
||||
justifyContent: 'flex-start',
|
||||
})
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'border': '1px solid rgba(0, 0, 0, 0.1)',
|
||||
'padding': '10px',
|
||||
}
|
||||
})
|
||||
.component();
|
||||
|
||||
const statusContainerTitle = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.DATABASE_MIGRATION_STATUS,
|
||||
width: '100%',
|
||||
CSSStyles: { ...styles.SECTION_HEADER_CSS }
|
||||
}).component();
|
||||
|
||||
this._refreshButton = view.modelBuilder.button()
|
||||
.withProps({
|
||||
label: loc.REFRESH,
|
||||
iconPath: IconPathHelper.refresh,
|
||||
iconHeight: 16,
|
||||
iconWidth: 16,
|
||||
width: 70,
|
||||
CSSStyles: { 'float': 'right' }
|
||||
}).component();
|
||||
|
||||
const statusHeadingContainer = view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
statusContainerTitle,
|
||||
this._refreshButton,
|
||||
]).withLayout({
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexFlow: 'row',
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this._refreshButton.onDidClick(async (e) => {
|
||||
this._refreshButton.enabled = false;
|
||||
await this.refresh();
|
||||
this._refreshButton.enabled = true;
|
||||
}));
|
||||
|
||||
const buttonContainer = view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'justify-content': 'left',
|
||||
'align-iems': 'center',
|
||||
},
|
||||
})
|
||||
.component();
|
||||
|
||||
buttonContainer.addItem(
|
||||
await this._createServiceSelector(view));
|
||||
|
||||
this._selectServiceText = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.SELECT_SERVICE_MESSAGE,
|
||||
CSSStyles: {
|
||||
'font-size': '12px',
|
||||
'margin': '10px',
|
||||
'font-weight': '350',
|
||||
'text-align': 'center',
|
||||
'display': 'none'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const header = view.modelBuilder.flexContainer()
|
||||
.withItems([statusHeadingContainer, buttonContainer])
|
||||
.withLayout({ flexFlow: 'column', })
|
||||
.component();
|
||||
|
||||
this._migrationStatusCardsContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
height: '272px',
|
||||
})
|
||||
.withProps({ CSSStyles: { 'overflow': 'hidden auto' } })
|
||||
.component();
|
||||
|
||||
await this._updateSummaryStatus();
|
||||
|
||||
// in progress
|
||||
this._inProgressMigrationButton = this._createStatusCard(
|
||||
view,
|
||||
IconPathHelper.inProgressMigration,
|
||||
loc.MIGRATION_IN_PROGRESS);
|
||||
this.disposables.push(
|
||||
this._inProgressMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ONGOING)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._inProgressMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// in progress warning
|
||||
this._inProgressWarningMigrationButton = this._createStatusCard(
|
||||
view,
|
||||
IconPathHelper.inProgressMigration,
|
||||
loc.MIGRATION_IN_PROGRESS,
|
||||
true);
|
||||
this.disposables.push(
|
||||
this._inProgressWarningMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ONGOING)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._inProgressWarningMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// successful
|
||||
this._successfulMigrationButton = this._createStatusCard(
|
||||
view,
|
||||
IconPathHelper.completedMigration,
|
||||
loc.MIGRATION_COMPLETED);
|
||||
this.disposables.push(
|
||||
this._successfulMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.SUCCEEDED)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._successfulMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// completing
|
||||
this._completingMigrationButton = this._createStatusCard(
|
||||
view,
|
||||
IconPathHelper.completingCutover,
|
||||
loc.MIGRATION_CUTOVER_CARD);
|
||||
this.disposables.push(
|
||||
this._completingMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.COMPLETING)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._completingMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// failed
|
||||
this._failedMigrationButton = this._createStatusCard(
|
||||
view,
|
||||
IconPathHelper.error,
|
||||
loc.MIGRATION_FAILED);
|
||||
this.disposables.push(
|
||||
this._failedMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.FAILED)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._failedMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// all migrations
|
||||
this._allMigrationButton = this._createStatusCard(
|
||||
view,
|
||||
IconPathHelper.view,
|
||||
loc.VIEW_ALL);
|
||||
this.disposables.push(
|
||||
this._allMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ALL)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._allMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
this._migrationStatusCardLoadingContainer = view.modelBuilder.loadingComponent()
|
||||
.withItem(this._migrationStatusCardsContainer)
|
||||
.component();
|
||||
statusContainer.addItem(header, { CSSStyles: { 'margin-bottom': '10px' } });
|
||||
statusContainer.addItem(this._selectServiceText, {});
|
||||
statusContainer.addItem(this._migrationStatusCardLoadingContainer, {});
|
||||
return statusContainer;
|
||||
}
|
||||
|
||||
private async _createServiceSelector(view: azdata.ModelView): Promise<azdata.Component> {
|
||||
const serviceContextLabel = await getSelectedServiceStatus();
|
||||
this._serviceContextButton = view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.sqlMigrationService,
|
||||
iconHeight: 22,
|
||||
iconWidth: 22,
|
||||
label: serviceContextLabel,
|
||||
title: serviceContextLabel,
|
||||
description: loc.MIGRATION_SERVICE_DESCRIPTION,
|
||||
buttonType: azdata.ButtonType.Informational,
|
||||
width: 375,
|
||||
CSSStyles: { ...BUTTON_CSS },
|
||||
})
|
||||
.component();
|
||||
|
||||
this.disposables.push(
|
||||
this._serviceContextButton.onDidClick(async () => {
|
||||
const dialog = new SelectMigrationServiceDialog(async () => await this.onDialogClosed());
|
||||
await dialog.initialize();
|
||||
}));
|
||||
|
||||
return this._serviceContextButton;
|
||||
}
|
||||
|
||||
private _updateStatusCard(
|
||||
migrations: DatabaseMigration[],
|
||||
card: StatusCard,
|
||||
status: AdsMigrationStatus,
|
||||
show?: boolean): void {
|
||||
const list = filterMigrations(migrations, status);
|
||||
const count = list?.length || 0;
|
||||
card.container.display = count > 0 || show ? '' : 'none';
|
||||
card.count.value = count.toString();
|
||||
}
|
||||
|
||||
private async _updateSummaryStatus(): Promise<void> {
|
||||
const serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
|
||||
const isContextValid = isServiceContextValid(serviceContext);
|
||||
await this._selectServiceText.updateCssStyles({ 'display': isContextValid ? 'none' : 'block' });
|
||||
await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': isContextValid ? 'visible' : 'hidden' });
|
||||
this._refreshButton.enabled = isContextValid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as loc from '../constants/strings';
|
||||
import { getSqlServerName, getMigrationStatusImage } from '../api/utils';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
|
||||
import { getResourceName } from '../api/azure';
|
||||
import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
|
||||
import { EmptySettingValue } from './tabBase';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
|
||||
const MigrationDetailsBlobContainerTabId = 'MigrationDetailsBlobContainerTab';
|
||||
|
||||
export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase<MigrationDetailsBlobContainerTab> {
|
||||
private _sourceDatabaseInfoField!: InfoFieldSchema;
|
||||
private _sourceDetailsInfoField!: InfoFieldSchema;
|
||||
private _sourceVersionInfoField!: InfoFieldSchema;
|
||||
private _targetDatabaseInfoField!: InfoFieldSchema;
|
||||
private _targetServerInfoField!: InfoFieldSchema;
|
||||
private _targetVersionInfoField!: InfoFieldSchema;
|
||||
private _migrationStatusInfoField!: InfoFieldSchema;
|
||||
private _backupLocationInfoField!: InfoFieldSchema;
|
||||
private _lastAppliedBackupInfoField!: InfoFieldSchema;
|
||||
private _currentRestoringFileInfoField!: InfoFieldSchema;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.id = MigrationDetailsBlobContainerTabId;
|
||||
}
|
||||
|
||||
public async create(
|
||||
context: vscode.ExtensionContext,
|
||||
view: azdata.ModelView,
|
||||
onClosedCallback: () => Promise<void>,
|
||||
statusBar: DashboardStatusBar,
|
||||
): Promise<MigrationDetailsBlobContainerTab> {
|
||||
|
||||
this.view = view;
|
||||
this.context = context;
|
||||
this.onClosedCallback = onClosedCallback;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize(this.view);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
if (this.isRefreshing || this.model?.migration === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this.refreshButton.enabled = false;
|
||||
this.refreshLoader.loading = true;
|
||||
await this.statusBar.clearError();
|
||||
|
||||
try {
|
||||
await this.model.fetchStatus();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
e.message);
|
||||
}
|
||||
|
||||
const migration = this.model?.migration;
|
||||
await this.cutoverButton.updateCssStyles(
|
||||
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
|
||||
|
||||
await this.showMigrationErrors(migration);
|
||||
|
||||
const sqlServerName = migration.properties.sourceServerName;
|
||||
const sourceDatabaseName = migration.properties.sourceDatabaseName;
|
||||
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
|
||||
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
|
||||
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
|
||||
const targetDatabaseName = migration.name;
|
||||
const targetServerName = getResourceName(migration.properties.scope);
|
||||
|
||||
const targetType = getMigrationTargetTypeEnum(migration);
|
||||
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
|
||||
|
||||
this.databaseLabel.value = sourceDatabaseName;
|
||||
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
|
||||
this._sourceDetailsInfoField.text.value = sqlServerName;
|
||||
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
|
||||
|
||||
this._targetDatabaseInfoField.text.value = targetDatabaseName;
|
||||
this._targetServerInfoField.text.value = targetServerName;
|
||||
this._targetVersionInfoField.text.value = targetServerVersion;
|
||||
|
||||
this._migrationStatusInfoField.text.value = getMigrationStatus(migration) ?? EmptySettingValue;
|
||||
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
|
||||
|
||||
const storageAccountResourceId = migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId;
|
||||
const blobContainerName
|
||||
= migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName
|
||||
?? migration.properties.migrationStatusDetails?.blobContainerName;
|
||||
|
||||
const backupLocation = storageAccountResourceId && blobContainerName
|
||||
? `${storageAccountResourceId?.split('/').pop()} - ${blobContainerName}`
|
||||
: blobContainerName;
|
||||
this._backupLocationInfoField.text.value = backupLocation ?? EmptySettingValue;
|
||||
this._lastAppliedBackupInfoField.text.value = migration.properties.migrationStatusDetails?.lastRestoredFilename ?? EmptySettingValue;
|
||||
this._currentRestoringFileInfoField.text.value = this.getMigrationCurrentlyRestoringFile(migration) ?? EmptySettingValue;
|
||||
|
||||
this.cutoverButton.enabled = canCutoverMigration(migration);
|
||||
this.cancelButton.enabled = canCancelMigration(migration);
|
||||
this.retryButton.enabled = canRetryMigration(migration);
|
||||
|
||||
this.isRefreshing = false;
|
||||
this.refreshLoader.loading = false;
|
||||
this.refreshButton.enabled = true;
|
||||
}
|
||||
|
||||
protected async initialize(view: azdata.ModelView): Promise<void> {
|
||||
try {
|
||||
const formItems: azdata.FormComponent<azdata.Component>[] = [
|
||||
{ component: this.createMigrationToolbarContainer() },
|
||||
{ component: await this.migrationInfoGrid() },
|
||||
{
|
||||
component: this.view.modelBuilder.separator()
|
||||
.withProps({ width: '100%', CSSStyles: { 'padding': '0' } })
|
||||
.component()
|
||||
},
|
||||
];
|
||||
|
||||
this.content = this.view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
formItems,
|
||||
{ horizontal: false })
|
||||
.withLayout({ width: '100%', padding: '0 0 0 15px' })
|
||||
.withProps({ width: '100%', CSSStyles: { padding: '0 0 0 15px' } })
|
||||
.component();
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async migrationInfoGrid(): Promise<azdata.FlexContainer> {
|
||||
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
|
||||
container.addItem(
|
||||
infoField.flexContainer,
|
||||
{ CSSStyles: { width: infoFieldWidth } });
|
||||
};
|
||||
|
||||
const flexServer = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
|
||||
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
|
||||
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
|
||||
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
|
||||
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
|
||||
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
|
||||
|
||||
const flexTarget = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
|
||||
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
|
||||
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
|
||||
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
|
||||
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
|
||||
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
|
||||
|
||||
const flexStatus = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
|
||||
this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
|
||||
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
|
||||
addInfoFieldToContainer(this._backupLocationInfoField, flexStatus);
|
||||
|
||||
const flexFile = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
|
||||
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', false);
|
||||
addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
|
||||
addInfoFieldToContainer(this._currentRestoringFileInfoField, flexFile);
|
||||
|
||||
const flexInfoProps = {
|
||||
flex: '0',
|
||||
CSSStyles: { 'flex': '0', 'width': infoFieldWidth }
|
||||
};
|
||||
|
||||
const flexInfo = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexWrap: 'wrap' })
|
||||
.withProps({ width: '100%' })
|
||||
.component();
|
||||
flexInfo.addItem(flexServer, flexInfoProps);
|
||||
flexInfo.addItem(flexTarget, flexInfoProps);
|
||||
flexInfo.addItem(flexStatus, flexInfoProps);
|
||||
flexInfo.addItem(flexFile, flexInfoProps);
|
||||
|
||||
return flexInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import * as loc from '../constants/strings';
|
||||
import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage } from '../api/utils';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import * as styles from '../constants/styles';
|
||||
import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
|
||||
import { getResourceName } from '../api/azure';
|
||||
import { EmptySettingValue } from './tabBase';
|
||||
import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
|
||||
const MigrationDetailsFileShareTabId = 'MigrationDetailsFileShareTab';
|
||||
|
||||
interface ActiveBackupFileSchema {
|
||||
fileName: string,
|
||||
type: string,
|
||||
status: string,
|
||||
dataUploaded: string,
|
||||
copyThroughput: string,
|
||||
backupStartTime: string,
|
||||
firstLSN: string,
|
||||
lastLSN: string
|
||||
}
|
||||
|
||||
export class MigrationDetailsFileShareTab extends MigrationDetailsTabBase<MigrationDetailsFileShareTab> {
|
||||
private _sourceDatabaseInfoField!: InfoFieldSchema;
|
||||
private _sourceDetailsInfoField!: InfoFieldSchema;
|
||||
private _sourceVersionInfoField!: InfoFieldSchema;
|
||||
private _targetDatabaseInfoField!: InfoFieldSchema;
|
||||
private _targetServerInfoField!: InfoFieldSchema;
|
||||
private _targetVersionInfoField!: InfoFieldSchema;
|
||||
private _migrationStatusInfoField!: InfoFieldSchema;
|
||||
private _fullBackupFileOnInfoField!: InfoFieldSchema;
|
||||
private _backupLocationInfoField!: InfoFieldSchema;
|
||||
private _lastLSNInfoField!: InfoFieldSchema;
|
||||
private _lastAppliedBackupInfoField!: InfoFieldSchema;
|
||||
private _lastAppliedBackupTakenOnInfoField!: InfoFieldSchema;
|
||||
private _currentRestoringFileInfoField!: InfoFieldSchema;
|
||||
|
||||
private _fileCount!: azdata.TextComponent;
|
||||
private _fileTable!: azdata.TableComponent;
|
||||
private _emptyTableFill!: azdata.FlexContainer;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.id = MigrationDetailsFileShareTabId;
|
||||
}
|
||||
|
||||
public async create(
|
||||
context: vscode.ExtensionContext,
|
||||
view: azdata.ModelView,
|
||||
onClosedCallback: () => Promise<void>,
|
||||
statusBar: DashboardStatusBar): Promise<MigrationDetailsFileShareTab> {
|
||||
|
||||
this.view = view;
|
||||
this.context = context;
|
||||
this.onClosedCallback = onClosedCallback;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize(this.view);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
if (this.isRefreshing || this.model?.migration === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this.refreshButton.enabled = false;
|
||||
this.refreshLoader.loading = true;
|
||||
await this.statusBar.clearError();
|
||||
await this._fileTable.updateProperty('data', []);
|
||||
|
||||
try {
|
||||
await this.model.fetchStatus();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
e.message);
|
||||
}
|
||||
|
||||
const migration = this.model?.migration;
|
||||
await this.cutoverButton.updateCssStyles(
|
||||
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
|
||||
|
||||
await this.showMigrationErrors(migration);
|
||||
|
||||
const sqlServerName = migration.properties.sourceServerName;
|
||||
const sourceDatabaseName = migration.properties.sourceDatabaseName;
|
||||
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
|
||||
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
|
||||
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
|
||||
const targetDatabaseName = migration.name;
|
||||
const targetServerName = getResourceName(migration.properties.scope);
|
||||
|
||||
const targetType = getMigrationTargetTypeEnum(migration);
|
||||
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
|
||||
|
||||
let lastAppliedSSN: string;
|
||||
let lastAppliedBackupFileTakenOn: string;
|
||||
|
||||
const tableData: ActiveBackupFileSchema[] = [];
|
||||
migration.properties.migrationStatusDetails?.activeBackupSets?.forEach(
|
||||
(activeBackupSet) => {
|
||||
tableData.push(
|
||||
...activeBackupSet.listOfBackupFiles.map(f => {
|
||||
return {
|
||||
fileName: f.fileName,
|
||||
type: activeBackupSet.backupType,
|
||||
status: f.status,
|
||||
dataUploaded: `${convertByteSizeToReadableUnit(f.dataWritten)} / ${convertByteSizeToReadableUnit(f.totalSize)}`,
|
||||
copyThroughput: (f.copyThroughput) ? (f.copyThroughput / 1024).toFixed(2) : EmptySettingValue,
|
||||
backupStartTime: activeBackupSet.backupStartDate,
|
||||
firstLSN: activeBackupSet.firstLSN,
|
||||
lastLSN: activeBackupSet.lastLSN
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
if (activeBackupSet.listOfBackupFiles[0].fileName === migration.properties.migrationStatusDetails?.lastRestoredFilename) {
|
||||
lastAppliedSSN = activeBackupSet.lastLSN;
|
||||
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
|
||||
}
|
||||
});
|
||||
|
||||
this.databaseLabel.value = sourceDatabaseName;
|
||||
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
|
||||
this._sourceDetailsInfoField.text.value = sqlServerName;
|
||||
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
|
||||
|
||||
this._targetDatabaseInfoField.text.value = targetDatabaseName;
|
||||
this._targetServerInfoField.text.value = targetServerName;
|
||||
this._targetVersionInfoField.text.value = targetServerVersion;
|
||||
|
||||
this._migrationStatusInfoField.text.value = getMigrationStatus(migration) ?? EmptySettingValue;
|
||||
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
|
||||
|
||||
this._fullBackupFileOnInfoField.text.value = migration?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? EmptySettingValue;
|
||||
|
||||
const fileShare = migration.properties.backupConfiguration?.sourceLocation?.fileShare;
|
||||
const backupLocation = fileShare?.path! ?? EmptySettingValue;
|
||||
this._backupLocationInfoField.text.value = backupLocation ?? EmptySettingValue;
|
||||
|
||||
this._lastLSNInfoField.text.value = lastAppliedSSN! ?? EmptySettingValue;
|
||||
this._lastAppliedBackupInfoField.text.value = migration.properties.migrationStatusDetails?.lastRestoredFilename ?? EmptySettingValue;
|
||||
this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : EmptySettingValue;
|
||||
this._currentRestoringFileInfoField.text.value = this.getMigrationCurrentlyRestoringFile(migration) ?? EmptySettingValue;
|
||||
|
||||
await this._fileCount.updateCssStyles({ ...styles.SECTION_HEADER_CSS, display: 'inline' });
|
||||
|
||||
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
|
||||
if (tableData.length === 0) {
|
||||
await this._emptyTableFill.updateCssStyles({ 'display': 'flex' });
|
||||
this._fileTable.height = '50px';
|
||||
await this._fileTable.updateProperty('data', []);
|
||||
} else {
|
||||
await this._emptyTableFill.updateCssStyles({ 'display': 'none' });
|
||||
this._fileTable.height = '300px';
|
||||
|
||||
// Sorting files in descending order of backupStartTime
|
||||
tableData.sort((file1, file2) => new Date(file1.backupStartTime) > new Date(file2.backupStartTime) ? - 1 : 1);
|
||||
}
|
||||
|
||||
const data = tableData.map(row => [
|
||||
row.fileName,
|
||||
row.type,
|
||||
row.status,
|
||||
row.dataUploaded,
|
||||
row.copyThroughput,
|
||||
convertIsoTimeToLocalTime(row.backupStartTime).toLocaleString(),
|
||||
row.firstLSN,
|
||||
row.lastLSN
|
||||
]) || [];
|
||||
|
||||
await this._fileTable.updateProperty('data', data);
|
||||
|
||||
this.cutoverButton.enabled = canCutoverMigration(migration);
|
||||
this.cancelButton.enabled = canCancelMigration(migration);
|
||||
this.retryButton.enabled = canRetryMigration(migration);
|
||||
this.isRefreshing = false;
|
||||
this.refreshLoader.loading = false;
|
||||
this.refreshButton.enabled = true;
|
||||
}
|
||||
|
||||
protected async initialize(view: azdata.ModelView): Promise<void> {
|
||||
try {
|
||||
this._fileCount = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
width: '500px',
|
||||
CSSStyles: { ...styles.BODY_CSS }
|
||||
}).component();
|
||||
|
||||
this._fileTable = this.view.modelBuilder.table()
|
||||
.withProps({
|
||||
ariaLabel: loc.ACTIVE_BACKUP_FILES,
|
||||
CSSStyles: { 'padding-left': '0px', 'max-width': '1020px' },
|
||||
data: [],
|
||||
height: '300px',
|
||||
columns: [
|
||||
{
|
||||
value: 'files',
|
||||
name: loc.ACTIVE_BACKUP_FILES,
|
||||
type: azdata.ColumnType.text,
|
||||
width: 230,
|
||||
},
|
||||
{
|
||||
value: 'type',
|
||||
name: loc.TYPE,
|
||||
width: 90,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: 'status',
|
||||
name: loc.STATUS,
|
||||
width: 60,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: 'uploaded',
|
||||
name: loc.DATA_UPLOADED,
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: 'throughput',
|
||||
name: loc.COPY_THROUGHPUT,
|
||||
width: 150,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: 'starttime',
|
||||
name: loc.BACKUP_START_TIME,
|
||||
width: 130,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: 'firstlsn',
|
||||
name: loc.FIRST_LSN,
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: 'lastlsn',
|
||||
name: loc.LAST_LSN,
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text,
|
||||
}
|
||||
],
|
||||
}).component();
|
||||
|
||||
const emptyTableImage = this.view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.emptyTable,
|
||||
iconHeight: '100px',
|
||||
iconWidth: '100px',
|
||||
height: '100px',
|
||||
width: '100px',
|
||||
CSSStyles: { 'text-align': 'center' }
|
||||
}).component();
|
||||
|
||||
const emptyTableText = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.EMPTY_TABLE_TEXT,
|
||||
CSSStyles: {
|
||||
...styles.NOTE_CSS,
|
||||
'margin-top': '8px',
|
||||
'text-align': 'center',
|
||||
'width': '300px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._emptyTableFill = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
alignItems: 'center'
|
||||
}).withItems([
|
||||
emptyTableImage,
|
||||
emptyTableText,
|
||||
]).withProps({
|
||||
width: '100%',
|
||||
display: 'none'
|
||||
}).component();
|
||||
|
||||
const formItems: azdata.FormComponent<azdata.Component>[] = [
|
||||
{ component: this.createMigrationToolbarContainer() },
|
||||
{ component: await this.migrationInfoGrid() },
|
||||
{
|
||||
component: this.view.modelBuilder.separator()
|
||||
.withProps({ width: '100%', CSSStyles: { 'padding': '0' } })
|
||||
.component()
|
||||
},
|
||||
{ component: this._fileCount },
|
||||
{ component: this._fileTable },
|
||||
{ component: this._emptyTableFill }
|
||||
];
|
||||
|
||||
const formContainer = this.view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
formItems,
|
||||
{ horizontal: false })
|
||||
.withLayout({ width: '100%', padding: '0 0 0 15px' })
|
||||
.withProps({ width: '100%', CSSStyles: { padding: '0 0 0 15px' } })
|
||||
.component();
|
||||
|
||||
this.content = formContainer;
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
|
||||
}
|
||||
}
|
||||
|
||||
protected async migrationInfoGrid(): Promise<azdata.FlexContainer> {
|
||||
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
|
||||
container.addItem(
|
||||
infoField.flexContainer,
|
||||
{ CSSStyles: { width: infoFieldWidth } });
|
||||
};
|
||||
|
||||
const flexServer = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
|
||||
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
|
||||
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
|
||||
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
|
||||
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
|
||||
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
|
||||
|
||||
const flexTarget = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
|
||||
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
|
||||
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
|
||||
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
|
||||
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
|
||||
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
|
||||
|
||||
const flexStatus = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
|
||||
this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', false);
|
||||
this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
|
||||
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
|
||||
addInfoFieldToContainer(this._fullBackupFileOnInfoField, flexStatus);
|
||||
addInfoFieldToContainer(this._backupLocationInfoField, flexStatus);
|
||||
|
||||
const flexFile = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._lastLSNInfoField = await this.createInfoField(loc.LAST_APPLIED_LSN, '', false);
|
||||
this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
|
||||
this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', false);
|
||||
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', false);
|
||||
addInfoFieldToContainer(this._lastLSNInfoField, flexFile);
|
||||
addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
|
||||
addInfoFieldToContainer(this._lastAppliedBackupTakenOnInfoField, flexFile);
|
||||
addInfoFieldToContainer(this._currentRestoringFileInfoField, flexFile);
|
||||
|
||||
const flexInfoProps = {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': infoFieldWidth
|
||||
}
|
||||
};
|
||||
|
||||
const flexInfo = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexWrap: 'wrap' })
|
||||
.withProps({ width: '100%' })
|
||||
.component();
|
||||
flexInfo.addItem(flexServer, flexInfoProps);
|
||||
flexInfo.addItem(flexTarget, flexInfoProps);
|
||||
flexInfo.addItem(flexStatus, flexInfoProps);
|
||||
flexInfo.addItem(flexFile, flexInfoProps);
|
||||
|
||||
return flexInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { MigrationServiceContext } from '../models/migrationLocalStorage';
|
||||
import * as loc from '../constants/strings';
|
||||
import * as styles from '../constants/styles';
|
||||
import { DatabaseMigration } from '../api/azure';
|
||||
import { TabBase } from './tabBase';
|
||||
import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migrationCutoverDialogModel';
|
||||
import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog';
|
||||
import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
|
||||
import { MigrationTargetType } from '../models/stateMachine';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
|
||||
export const infoFieldLgWidth: string = '330px';
|
||||
export const infoFieldWidth: string = '250px';
|
||||
|
||||
const statusImageSize: number = 14;
|
||||
|
||||
export const MigrationTargetTypeName: loc.LookupTable<string> = {
|
||||
[MigrationTargetType.SQLMI]: loc.AZURE_SQL_DATABASE_MANAGED_INSTANCE,
|
||||
[MigrationTargetType.SQLVM]: loc.AZURE_SQL_DATABASE_VIRTUAL_MACHINE,
|
||||
[MigrationTargetType.SQLDB]: loc.AZURE_SQL_DATABASE,
|
||||
};
|
||||
|
||||
export interface InfoFieldSchema {
|
||||
flexContainer: azdata.FlexContainer,
|
||||
text: azdata.TextComponent,
|
||||
icon?: azdata.ImageComponent,
|
||||
}
|
||||
|
||||
export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
|
||||
protected model!: MigrationCutoverDialogModel;
|
||||
protected databaseLabel!: azdata.TextComponent;
|
||||
protected serviceContext!: MigrationServiceContext;
|
||||
protected onClosedCallback!: () => Promise<void>;
|
||||
|
||||
protected cutoverButton!: azdata.ButtonComponent;
|
||||
protected refreshButton!: azdata.ButtonComponent;
|
||||
protected cancelButton!: azdata.ButtonComponent;
|
||||
protected refreshLoader!: azdata.LoadingComponent;
|
||||
protected copyDatabaseMigrationDetails!: azdata.ButtonComponent;
|
||||
protected newSupportRequest!: azdata.ButtonComponent;
|
||||
protected retryButton!: azdata.ButtonComponent;
|
||||
protected summaryTextComponent: azdata.TextComponent[] = [];
|
||||
|
||||
public abstract create(context: vscode.ExtensionContext, view: azdata.ModelView, onClosedCallback: () => Promise<void>, statusBar: DashboardStatusBar): Promise<T>;
|
||||
|
||||
protected abstract migrationInfoGrid(): Promise<azdata.FlexContainer>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.title = '';
|
||||
}
|
||||
|
||||
public async setMigrationContext(
|
||||
serviceContext: MigrationServiceContext,
|
||||
migration: DatabaseMigration): Promise<void> {
|
||||
this.serviceContext = serviceContext;
|
||||
this.model = new MigrationCutoverDialogModel(serviceContext, migration);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
protected createBreadcrumbContainer(): azdata.FlexContainer {
|
||||
const migrationsTabLink = this.view.modelBuilder.hyperlink()
|
||||
.withProps({
|
||||
label: loc.BREADCRUMB_MIGRATIONS,
|
||||
url: '',
|
||||
title: loc.BREADCRUMB_MIGRATIONS,
|
||||
CSSStyles: {
|
||||
'padding': '5px 5px 5px 0',
|
||||
'font-size': '13px'
|
||||
}
|
||||
})
|
||||
.component();
|
||||
this.disposables.push(
|
||||
migrationsTabLink.onDidClick(
|
||||
async (e) => await this.onClosedCallback()));
|
||||
|
||||
const breadCrumbImage = this.view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.breadCrumb,
|
||||
iconHeight: 8,
|
||||
iconWidth: 8,
|
||||
width: 8,
|
||||
height: 8,
|
||||
CSSStyles: { 'padding': '4px' }
|
||||
}).component();
|
||||
|
||||
this.databaseLabel = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
textType: azdata.TextType.Normal,
|
||||
value: '...',
|
||||
CSSStyles: {
|
||||
'font-size': '16px',
|
||||
'font-weight': '600',
|
||||
'margin-block-start': '0',
|
||||
'margin-block-end': '0',
|
||||
}
|
||||
}).component();
|
||||
|
||||
return this.view.modelBuilder.flexContainer()
|
||||
.withItems(
|
||||
[migrationsTabLink, breadCrumbImage, this.databaseLabel],
|
||||
{ flex: '0 0 auto' })
|
||||
.withLayout({
|
||||
flexFlow: 'row',
|
||||
alignItems: 'center',
|
||||
alignContent: 'center',
|
||||
})
|
||||
.withProps({
|
||||
height: 20,
|
||||
CSSStyles: { 'padding': '0', 'margin-bottom': '5px' }
|
||||
})
|
||||
.component();
|
||||
}
|
||||
|
||||
protected createMigrationToolbarContainer(): azdata.FlexContainer {
|
||||
const toolbarContainer = this.view.modelBuilder.toolbarContainer();
|
||||
const buttonHeight = 20;
|
||||
this.cutoverButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.cutover,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
label: loc.COMPLETE_CUTOVER,
|
||||
height: buttonHeight,
|
||||
enabled: false,
|
||||
CSSStyles: { 'display': 'none' }
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.cutoverButton.onDidClick(async (e) => {
|
||||
await this.statusBar.clearError();
|
||||
await this.refresh();
|
||||
const dialog = new ConfirmCutoverDialog(this.model);
|
||||
await dialog.initialize();
|
||||
|
||||
if (this.model.CutoverError) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_CUTOVER_ERROR,
|
||||
loc.MIGRATION_CUTOVER_ERROR,
|
||||
this.model.CutoverError.message);
|
||||
}
|
||||
}));
|
||||
|
||||
this.cancelButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.cancel,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
label: loc.CANCEL_MIGRATION,
|
||||
height: buttonHeight,
|
||||
enabled: false,
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.cancelButton.onDidClick((e) => {
|
||||
void vscode.window.showInformationMessage(
|
||||
loc.CANCEL_MIGRATION_CONFIRMATION,
|
||||
{ modal: true },
|
||||
loc.YES,
|
||||
loc.NO
|
||||
).then(async (v) => {
|
||||
if (v === loc.YES) {
|
||||
await this.statusBar.clearError();
|
||||
await this.model.cancelMigration();
|
||||
await this.refresh();
|
||||
if (this.model.CancelMigrationError) {
|
||||
{
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_CANCELLATION_ERROR,
|
||||
loc.MIGRATION_CANCELLATION_ERROR,
|
||||
this.model.CancelMigrationError.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
this.retryButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
label: loc.RETRY_MIGRATION,
|
||||
iconPath: IconPathHelper.retry,
|
||||
enabled: false,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
height: buttonHeight,
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.retryButton.onDidClick(
|
||||
async (e) => {
|
||||
await this.refresh();
|
||||
const retryMigrationDialog = new RetryMigrationDialog(
|
||||
this.context,
|
||||
this.serviceContext,
|
||||
this.model.migration,
|
||||
this.onClosedCallback);
|
||||
await retryMigrationDialog.openDialog();
|
||||
}
|
||||
));
|
||||
|
||||
this.copyDatabaseMigrationDetails = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.copy,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
label: loc.COPY_MIGRATION_DETAILS,
|
||||
height: buttonHeight,
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.copyDatabaseMigrationDetails.onDidClick(async (e) => {
|
||||
await this.refresh();
|
||||
await vscode.env.clipboard.writeText(this._getMigrationDetails());
|
||||
|
||||
void vscode.window.showInformationMessage(loc.DETAILS_COPIED);
|
||||
}));
|
||||
|
||||
this.newSupportRequest = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
label: loc.NEW_SUPPORT_REQUEST,
|
||||
iconPath: IconPathHelper.newSupportRequest,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
height: buttonHeight,
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.newSupportRequest.onDidClick(async (e) => {
|
||||
const serviceId = this.model.migration.properties.migrationService;
|
||||
const supportUrl = `https://portal.azure.com/#resource${serviceId}/supportrequest`;
|
||||
await vscode.env.openExternal(vscode.Uri.parse(supportUrl));
|
||||
}));
|
||||
|
||||
this.refreshButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.refresh,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
label: loc.REFRESH_BUTTON_TEXT,
|
||||
height: buttonHeight,
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.refreshButton.onDidClick(
|
||||
async (e) => await this.refresh()));
|
||||
|
||||
this.refreshLoader = this.view.modelBuilder.loadingComponent()
|
||||
.withProps({
|
||||
loading: false,
|
||||
CSSStyles: {
|
||||
'height': '8px',
|
||||
'margin-top': '4px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
toolbarContainer.addToolbarItems([
|
||||
<azdata.ToolbarComponent>{ component: this.cutoverButton },
|
||||
<azdata.ToolbarComponent>{ component: this.cancelButton },
|
||||
<azdata.ToolbarComponent>{ component: this.retryButton },
|
||||
<azdata.ToolbarComponent>{ component: this.copyDatabaseMigrationDetails, toolbarSeparatorAfter: true },
|
||||
<azdata.ToolbarComponent>{ component: this.newSupportRequest, toolbarSeparatorAfter: true },
|
||||
<azdata.ToolbarComponent>{ component: this.refreshButton },
|
||||
<azdata.ToolbarComponent>{ component: this.refreshLoader },
|
||||
]);
|
||||
|
||||
return this.view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
this.createBreadcrumbContainer(),
|
||||
toolbarContainer.component(),
|
||||
])
|
||||
.withLayout({ flexFlow: 'column', width: '100%' })
|
||||
.component();
|
||||
}
|
||||
|
||||
protected async createInfoCard(
|
||||
label: string,
|
||||
iconPath: azdata.IconPath
|
||||
): Promise<azdata.FlexContainer> {
|
||||
const defaultValue = (0).toLocaleString();
|
||||
const flexContainer = this.view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
width: 168,
|
||||
CSSStyles: {
|
||||
'flex-direction': 'column',
|
||||
'margin': '0 12px 0 0',
|
||||
'box-sizing': 'border-box',
|
||||
'border': '1px solid rgba(204, 204, 204, 0.5)',
|
||||
'box-shadow': '0px 2px 4px rgba(0, 0, 0, 0.1)',
|
||||
'border-radius': '2px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const labelComponent = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: label,
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'font-weight': '600',
|
||||
'margin': '5px',
|
||||
}
|
||||
}).component();
|
||||
flexContainer.addItem(labelComponent);
|
||||
|
||||
const iconComponent = this.view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: iconPath,
|
||||
iconHeight: 16,
|
||||
iconWidth: 16,
|
||||
height: 16,
|
||||
width: 16,
|
||||
CSSStyles: {
|
||||
'margin': '5px 5px 5px 5px',
|
||||
'padding': '0'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const textComponent = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: defaultValue,
|
||||
title: defaultValue,
|
||||
CSSStyles: {
|
||||
'font-size': '20px',
|
||||
'font-weight': '600',
|
||||
'margin': '0 5px 0 5px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this.summaryTextComponent.push(textComponent);
|
||||
|
||||
const iconTextComponent = this.view.modelBuilder.flexContainer()
|
||||
.withItems([iconComponent, textComponent])
|
||||
.withLayout({ alignItems: 'center' })
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'row',
|
||||
'margin': '0 0 0 5px',
|
||||
'padding': '0',
|
||||
},
|
||||
display: 'inline-flex'
|
||||
}).component();
|
||||
|
||||
flexContainer.addItem(iconTextComponent);
|
||||
|
||||
return flexContainer;
|
||||
}
|
||||
|
||||
protected async createInfoField(label: string, value: string, defaultHidden: boolean = false, iconPath?: azdata.IconPath): Promise<{
|
||||
flexContainer: azdata.FlexContainer,
|
||||
text: azdata.TextComponent,
|
||||
icon?: azdata.ImageComponent
|
||||
}> {
|
||||
const flexContainer = this.view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'column',
|
||||
'padding-right': '12px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const labelComponent = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: label,
|
||||
CSSStyles: {
|
||||
...styles.LIGHT_LABEL_CSS,
|
||||
'margin-bottom': '0',
|
||||
}
|
||||
}).component();
|
||||
flexContainer.addItem(labelComponent);
|
||||
|
||||
const textComponent = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: value,
|
||||
title: value,
|
||||
description: value,
|
||||
width: '100%',
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'line-height': '18px',
|
||||
'margin': '4px 0 12px',
|
||||
'overflow': 'hidden',
|
||||
'text-overflow': 'ellipsis',
|
||||
'max-width': '230px',
|
||||
'display': 'inline-block',
|
||||
}
|
||||
}).component();
|
||||
|
||||
let iconComponent;
|
||||
if (iconPath) {
|
||||
iconComponent = this.view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: (iconPath === ' ') ? undefined : iconPath,
|
||||
iconHeight: statusImageSize,
|
||||
iconWidth: statusImageSize,
|
||||
height: statusImageSize,
|
||||
width: statusImageSize,
|
||||
title: value,
|
||||
CSSStyles: {
|
||||
'margin': '7px 3px 0 0',
|
||||
'padding': '0'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const iconTextComponent = this.view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
iconComponent,
|
||||
textComponent
|
||||
]).withProps({
|
||||
CSSStyles: {
|
||||
'margin': '0',
|
||||
'padding': '0'
|
||||
},
|
||||
display: 'inline-flex'
|
||||
}).component();
|
||||
flexContainer.addItem(iconTextComponent);
|
||||
} else {
|
||||
flexContainer.addItem(textComponent);
|
||||
}
|
||||
|
||||
return {
|
||||
flexContainer: flexContainer,
|
||||
text: textComponent,
|
||||
icon: iconComponent
|
||||
};
|
||||
}
|
||||
|
||||
protected async showMigrationErrors(migration: DatabaseMigration): Promise<void> {
|
||||
const errorMessage = this.getMigrationErrors(migration);
|
||||
if (errorMessage?.length > 0) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_ERROR_DETAILS_TITLE,
|
||||
loc.MIGRATION_ERROR_DETAILS_LABEL,
|
||||
errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
protected getMigrationCurrentlyRestoringFile(migration: DatabaseMigration): string | undefined {
|
||||
const lastAppliedBackupFile = this.getMigrationLastAppliedBackupFile(migration);
|
||||
const currentRestoringFile = migration?.properties?.migrationStatusDetails?.currentRestoringFilename;
|
||||
|
||||
return currentRestoringFile === lastAppliedBackupFile
|
||||
&& currentRestoringFile && currentRestoringFile.length > 0
|
||||
? loc.ALL_BACKUPS_RESTORED
|
||||
: currentRestoringFile;
|
||||
}
|
||||
|
||||
protected getMigrationLastAppliedBackupFile(migration: DatabaseMigration): string | undefined {
|
||||
return migration?.properties?.migrationStatusDetails?.lastRestoredFilename
|
||||
|| migration?.properties?.offlineConfiguration?.lastBackupName;
|
||||
}
|
||||
|
||||
private _getMigrationDetails(): string {
|
||||
return JSON.stringify(this.model.migration, undefined, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,567 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as loc from '../constants/strings';
|
||||
import { getSqlServerName, getMigrationStatusImage, getPipelineStatusImage, debounce } from '../api/utils';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import { canCancelMigration, canCutoverMigration, canRetryMigration, formatDateTimeString, formatNumber, formatSizeBytes, formatSizeKb, formatTime, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation, PipelineStatusCodes } from '../constants/helper';
|
||||
import { CopyProgressDetail, getResourceName } from '../api/azure';
|
||||
import { InfoFieldSchema, infoFieldLgWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
|
||||
import { EmptySettingValue } from './tabBase';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
import { EOL } from 'os';
|
||||
|
||||
const MigrationDetailsTableTabId = 'MigrationDetailsTableTab';
|
||||
|
||||
const TableColumns = {
|
||||
tableName: 'tableName',
|
||||
status: 'status',
|
||||
dataRead: 'dataRead',
|
||||
dataWritten: 'dataWritten',
|
||||
rowsRead: 'rowsRead',
|
||||
rowsCopied: 'rowsCopied',
|
||||
copyThroughput: 'copyThroughput',
|
||||
copyDuration: 'copyDuration',
|
||||
parallelCopyType: 'parallelCopyType',
|
||||
usedParallelCopies: 'usedParallelCopies',
|
||||
copyStart: 'copyStart',
|
||||
};
|
||||
|
||||
enum SummaryCardIndex {
|
||||
TotalTables = 0,
|
||||
InProgressTables = 1,
|
||||
SuccessfulTables = 2,
|
||||
FailedTables = 3,
|
||||
CanceledTables = 4,
|
||||
}
|
||||
|
||||
export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationDetailsTableTab> {
|
||||
private _sourceDatabaseInfoField!: InfoFieldSchema;
|
||||
private _sourceDetailsInfoField!: InfoFieldSchema;
|
||||
private _sourceVersionInfoField!: InfoFieldSchema;
|
||||
private _targetDatabaseInfoField!: InfoFieldSchema;
|
||||
private _targetServerInfoField!: InfoFieldSchema;
|
||||
private _targetVersionInfoField!: InfoFieldSchema;
|
||||
private _migrationStatusInfoField!: InfoFieldSchema;
|
||||
private _serverObjectsInfoField!: InfoFieldSchema;
|
||||
private _tableFilterInputBox!: azdata.InputBoxComponent;
|
||||
private _columnSortDropdown!: azdata.DropDownComponent;
|
||||
private _columnSortCheckbox!: azdata.CheckBoxComponent;
|
||||
private _progressTable!: azdata.TableComponent;
|
||||
private _progressDetail: CopyProgressDetail[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.id = MigrationDetailsTableTabId;
|
||||
}
|
||||
|
||||
public async create(
|
||||
context: vscode.ExtensionContext,
|
||||
view: azdata.ModelView,
|
||||
onClosedCallback: () => Promise<void>,
|
||||
statusBar: DashboardStatusBar): Promise<MigrationDetailsTableTab> {
|
||||
|
||||
this.view = view;
|
||||
this.context = context;
|
||||
this.onClosedCallback = onClosedCallback;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize(this.view);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@debounce(500)
|
||||
public async refresh(): Promise<void> {
|
||||
if (this.isRefreshing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this.refreshButton.enabled = false;
|
||||
this.refreshLoader.loading = true;
|
||||
await this.statusBar.clearError();
|
||||
|
||||
try {
|
||||
await this.model.fetchStatus();
|
||||
await this._loadData();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
e.message);
|
||||
}
|
||||
|
||||
this.isRefreshing = false;
|
||||
this.refreshLoader.loading = false;
|
||||
this.refreshButton.enabled = true;
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
const migration = this.model?.migration;
|
||||
await this.showMigrationErrors(this.model?.migration);
|
||||
|
||||
await this.cutoverButton.updateCssStyles(
|
||||
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
|
||||
|
||||
const sqlServerName = migration?.properties.sourceServerName;
|
||||
const sourceDatabaseName = migration?.properties.sourceDatabaseName;
|
||||
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
|
||||
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
|
||||
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
|
||||
const targetDatabaseName = migration?.name;
|
||||
const targetServerName = getResourceName(migration?.properties.scope);
|
||||
|
||||
const targetType = getMigrationTargetTypeEnum(migration);
|
||||
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
|
||||
|
||||
const hashSet: loc.LookupTable<number> = {};
|
||||
this._progressDetail = migration?.properties.migrationStatusDetails?.listOfCopyProgressDetails ?? [];
|
||||
await this._populateTableData(hashSet);
|
||||
|
||||
const successCount = hashSet[PipelineStatusCodes.Succeeded] ?? 0;
|
||||
const cancelledCount =
|
||||
(hashSet[PipelineStatusCodes.Canceled] ?? 0) +
|
||||
(hashSet[PipelineStatusCodes.Cancelled] ?? 0);
|
||||
|
||||
const failedCount = hashSet[PipelineStatusCodes.Failed] ?? 0;
|
||||
const inProgressCount =
|
||||
(hashSet[PipelineStatusCodes.Queued] ?? 0) +
|
||||
(hashSet[PipelineStatusCodes.CopyFinished] ?? 0) +
|
||||
(hashSet[PipelineStatusCodes.Copying] ?? 0) +
|
||||
(hashSet[PipelineStatusCodes.PreparingForCopy] ?? 0) +
|
||||
(hashSet[PipelineStatusCodes.RebuildingIndexes] ?? 0) +
|
||||
(hashSet[PipelineStatusCodes.InProgress] ?? 0);
|
||||
|
||||
const totalCount = migration.properties.migrationStatusDetails?.listOfCopyProgressDetails.length ?? 0;
|
||||
|
||||
this._updateSummaryComponent(SummaryCardIndex.TotalTables, totalCount);
|
||||
this._updateSummaryComponent(SummaryCardIndex.InProgressTables, inProgressCount);
|
||||
this._updateSummaryComponent(SummaryCardIndex.SuccessfulTables, successCount);
|
||||
this._updateSummaryComponent(SummaryCardIndex.FailedTables, failedCount);
|
||||
this._updateSummaryComponent(SummaryCardIndex.CanceledTables, cancelledCount);
|
||||
|
||||
this.databaseLabel.value = sourceDatabaseName;
|
||||
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
|
||||
this._sourceDetailsInfoField.text.value = sqlServerName;
|
||||
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
|
||||
|
||||
this._targetDatabaseInfoField.text.value = targetDatabaseName;
|
||||
this._targetServerInfoField.text.value = targetServerName;
|
||||
this._targetVersionInfoField.text.value = targetServerVersion;
|
||||
|
||||
this._migrationStatusInfoField.text.value = getMigrationStatus(migration) ?? EmptySettingValue;
|
||||
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
|
||||
this._serverObjectsInfoField.text.value = totalCount.toLocaleString();
|
||||
|
||||
this.cutoverButton.enabled = canCutoverMigration(migration);
|
||||
this.cancelButton.enabled = canCancelMigration(migration);
|
||||
this.retryButton.enabled = canRetryMigration(migration);
|
||||
}
|
||||
|
||||
private async _populateTableData(hashSet: loc.LookupTable<number> = {}): Promise<void> {
|
||||
if (this._progressTable.data.length > 0) {
|
||||
await this._progressTable.updateProperty('data', []);
|
||||
}
|
||||
|
||||
// Sort table data
|
||||
this._sortTableMigrations(
|
||||
this._progressDetail,
|
||||
(<azdata.CategoryValue>this._columnSortDropdown.value).name,
|
||||
this._columnSortCheckbox.checked === true);
|
||||
|
||||
const data = this._progressDetail.map((d) => {
|
||||
hashSet[d.status] = (hashSet[d.status] ?? 0) + 1;
|
||||
return [
|
||||
d.tableName,
|
||||
<azdata.HyperlinkColumnCellValue>{
|
||||
icon: getPipelineStatusImage(d.status),
|
||||
title: loc.PipelineRunStatus[d.status] ?? d.status?.toUpperCase(),
|
||||
},
|
||||
formatSizeBytes(d.dataRead),
|
||||
formatSizeBytes(d.dataWritten),
|
||||
formatNumber(d.rowsRead),
|
||||
formatNumber(d.rowsCopied),
|
||||
formatSizeKb(d.copyThroughput),
|
||||
formatTime((d.copyDuration ?? 0) * 1000),
|
||||
loc.ParallelCopyType[d.parallelCopyType] ?? d.parallelCopyType,
|
||||
d.usedParallelCopies,
|
||||
formatDateTimeString(d.copyStart),
|
||||
];
|
||||
}) ?? [];
|
||||
|
||||
// Filter tableData
|
||||
const filteredData = this._filterTables(data, this._tableFilterInputBox.value);
|
||||
|
||||
await this._progressTable.updateProperty('data', filteredData);
|
||||
}
|
||||
|
||||
protected async initialize(view: azdata.ModelView): Promise<void> {
|
||||
try {
|
||||
this._progressTable = this.view.modelBuilder.table()
|
||||
.withProps({
|
||||
ariaLabel: loc.ACTIVE_BACKUP_FILES,
|
||||
CSSStyles: {
|
||||
'padding-left': '0px',
|
||||
'max-width': '1111px'
|
||||
},
|
||||
data: [],
|
||||
height: '300px',
|
||||
columns: [
|
||||
{
|
||||
value: TableColumns.tableName,
|
||||
name: loc.SQLDB_COL_TABLE_NAME,
|
||||
type: azdata.ColumnType.text,
|
||||
width: 170,
|
||||
},
|
||||
<azdata.HyperlinkColumn>{
|
||||
name: loc.STATUS,
|
||||
value: TableColumns.status,
|
||||
width: 106,
|
||||
type: azdata.ColumnType.hyperlink,
|
||||
icon: IconPathHelper.inProgressMigration,
|
||||
showText: true,
|
||||
},
|
||||
{
|
||||
value: TableColumns.dataRead,
|
||||
name: loc.SQLDB_COL_DATA_READ,
|
||||
width: 64,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: TableColumns.dataWritten,
|
||||
name: loc.SQLDB_COL_DATA_WRITTEN,
|
||||
width: 77,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: TableColumns.rowsRead,
|
||||
name: loc.SQLDB_COL_ROWS_READ,
|
||||
width: 68,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: TableColumns.rowsCopied,
|
||||
name: loc.SQLDB_COL_ROWS_COPIED,
|
||||
width: 77,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: TableColumns.copyThroughput,
|
||||
name: loc.SQLDB_COL_COPY_THROUGHPUT,
|
||||
width: 102,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: TableColumns.copyDuration,
|
||||
name: loc.SQLDB_COL_COPY_DURATION,
|
||||
width: 87,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: TableColumns.parallelCopyType,
|
||||
name: loc.SQLDB_COL_PARRALEL_COPY_TYPE,
|
||||
width: 104,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: TableColumns.usedParallelCopies,
|
||||
name: loc.SQLDB_COL_USED_PARALLEL_COPIES,
|
||||
width: 116,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: TableColumns.copyStart,
|
||||
name: loc.SQLDB_COL_COPY_START,
|
||||
width: 140,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
],
|
||||
}).component();
|
||||
|
||||
const formItems: azdata.FormComponent<azdata.Component>[] = [
|
||||
{ component: this.createMigrationToolbarContainer() },
|
||||
{ component: await this.migrationInfoGrid() },
|
||||
{
|
||||
component: this.view.modelBuilder.separator()
|
||||
.withProps({ width: '100%', CSSStyles: { 'padding': '0' } })
|
||||
.component()
|
||||
},
|
||||
{ component: await this._createStatusBar() },
|
||||
{ component: await this._createTableFilter() },
|
||||
{ component: this._progressTable },
|
||||
];
|
||||
|
||||
this.disposables.push(
|
||||
this._progressTable.onCellAction!(
|
||||
async (rowState: azdata.ICellActionEventArgs) => {
|
||||
const buttonState = <azdata.ICellActionEventArgs>rowState;
|
||||
if (buttonState?.column === 1) {
|
||||
const tableName = this._progressTable!.data[rowState.row][0] || null;
|
||||
const tableProgress = this.model.migration.properties.migrationStatusDetails?.listOfCopyProgressDetails?.find(
|
||||
progress => progress.tableName === tableName);
|
||||
const errors = tableProgress?.errors || [];
|
||||
const tableStatus = loc.PipelineRunStatus[tableProgress?.status ?? ''] ?? tableProgress?.status;
|
||||
const statusMessage = loc.TABLE_MIGRATION_STATUS_LABEL(tableStatus);
|
||||
const errorMessage = errors.join(EOL);
|
||||
|
||||
this.showDialogMessage(
|
||||
loc.TABLE_MIGRATION_STATUS_TITLE,
|
||||
statusMessage,
|
||||
errorMessage);
|
||||
}
|
||||
}));
|
||||
|
||||
const formContainer = this.view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
formItems,
|
||||
{ horizontal: false })
|
||||
.withProps({ width: '100%', CSSStyles: { margin: '0 0 0 5px', padding: '0 15px 0 15px' } })
|
||||
.component();
|
||||
|
||||
this.content = formContainer;
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
|
||||
}
|
||||
}
|
||||
|
||||
private _sortTableMigrations(data: CopyProgressDetail[], columnName: string, ascending: boolean): void {
|
||||
const sortDir = ascending ? -1 : 1;
|
||||
switch (columnName) {
|
||||
case TableColumns.tableName:
|
||||
data.sort((t1, t2) => this.stringCompare(t1.tableName, t2.tableName, sortDir));
|
||||
return;
|
||||
case TableColumns.status:
|
||||
data.sort((t1, t2) => this.stringCompare(t1.status, t2.status, sortDir));
|
||||
return;
|
||||
case TableColumns.dataRead:
|
||||
data.sort((t1, t2) => this.numberCompare(t1.dataRead, t2.dataRead, sortDir));
|
||||
return;
|
||||
case TableColumns.dataWritten:
|
||||
data.sort((t1, t2) => this.numberCompare(t1.dataWritten, t2.dataWritten, sortDir));
|
||||
return;
|
||||
case TableColumns.rowsRead:
|
||||
data.sort((t1, t2) => this.numberCompare(t1.rowsRead, t2.rowsRead, sortDir));
|
||||
return;
|
||||
case TableColumns.rowsCopied:
|
||||
data.sort((t1, t2) => this.numberCompare(t1.rowsCopied, t2.rowsCopied, sortDir));
|
||||
return;
|
||||
case TableColumns.copyThroughput:
|
||||
data.sort((t1, t2) => this.numberCompare(t1.copyThroughput, t2.copyThroughput, sortDir));
|
||||
return;
|
||||
case TableColumns.copyDuration:
|
||||
data.sort((t1, t2) => this.numberCompare(t1.copyDuration, t2.copyDuration, sortDir));
|
||||
return;
|
||||
case TableColumns.parallelCopyType:
|
||||
data.sort((t1, t2) => this.stringCompare(t1.parallelCopyType, t2.parallelCopyType, sortDir));
|
||||
return;
|
||||
case TableColumns.usedParallelCopies:
|
||||
data.sort((t1, t2) => this.numberCompare(t1.usedParallelCopies, t2.usedParallelCopies, sortDir));
|
||||
return;
|
||||
case TableColumns.copyStart:
|
||||
data.sort((t1, t2) => this.dateCompare(t1.copyStart, t2.copyStart, sortDir));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private _updateSummaryComponent(cardIndex: number, value: number): void {
|
||||
const stringValue = value.toLocaleString();
|
||||
const textComponent = this.summaryTextComponent[cardIndex];
|
||||
textComponent.value = stringValue;
|
||||
textComponent.title = stringValue;
|
||||
}
|
||||
|
||||
private _filterTables(tables: any[], value: string | undefined): any[] {
|
||||
const lcValue = value?.toLowerCase() ?? '';
|
||||
|
||||
return lcValue.length > 0
|
||||
? tables.filter((table: string[]) =>
|
||||
table.some((col: string | { title: string }) => {
|
||||
return typeof (col) === 'string'
|
||||
? col.toLowerCase().includes(lcValue)
|
||||
: col.title?.toLowerCase().includes(lcValue);
|
||||
}))
|
||||
: tables;
|
||||
}
|
||||
|
||||
private async _createTableFilter(): Promise<azdata.FlexContainer> {
|
||||
this._tableFilterInputBox = this.view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
inputType: 'text',
|
||||
maxLength: 100,
|
||||
width: 268,
|
||||
placeHolder: loc.FILTER_SERVER_OBJECTS_PLACEHOLDER,
|
||||
ariaLabel: loc.FILTER_SERVER_OBJECTS_ARIA_LABEL,
|
||||
})
|
||||
.component();
|
||||
|
||||
this.disposables.push(
|
||||
this._tableFilterInputBox.onTextChanged(
|
||||
async (value) => await this._populateTableData()));
|
||||
|
||||
const sortLabel = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.SORT_LABEL,
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'font-weight': '600',
|
||||
'margin': '3px 0 0 0',
|
||||
},
|
||||
}).component();
|
||||
|
||||
this._columnSortDropdown = this.view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
editable: false,
|
||||
width: 150,
|
||||
CSSStyles: { 'margin-left': '5px' },
|
||||
value: <azdata.CategoryValue>{ name: TableColumns.copyStart, displayName: loc.START_TIME },
|
||||
values: [
|
||||
<azdata.CategoryValue>{ name: TableColumns.tableName, displayName: loc.SQLDB_COL_TABLE_NAME },
|
||||
<azdata.CategoryValue>{ name: TableColumns.status, displayName: loc.STATUS },
|
||||
<azdata.CategoryValue>{ name: TableColumns.dataRead, displayName: loc.SQLDB_COL_DATA_READ },
|
||||
<azdata.CategoryValue>{ name: TableColumns.dataWritten, displayName: loc.SQLDB_COL_DATA_WRITTEN },
|
||||
<azdata.CategoryValue>{ name: TableColumns.rowsRead, displayName: loc.SQLDB_COL_ROWS_READ },
|
||||
<azdata.CategoryValue>{ name: TableColumns.rowsCopied, displayName: loc.SQLDB_COL_ROWS_COPIED },
|
||||
<azdata.CategoryValue>{ name: TableColumns.copyThroughput, displayName: loc.SQLDB_COL_COPY_THROUGHPUT },
|
||||
<azdata.CategoryValue>{ name: TableColumns.copyDuration, displayName: loc.SQLDB_COL_COPY_DURATION },
|
||||
<azdata.CategoryValue>{ name: TableColumns.parallelCopyType, displayName: loc.SQLDB_COL_PARRALEL_COPY_TYPE },
|
||||
<azdata.CategoryValue>{ name: TableColumns.usedParallelCopies, displayName: loc.SQLDB_COL_USED_PARALLEL_COPIES },
|
||||
<azdata.CategoryValue>{ name: TableColumns.copyStart, displayName: loc.SQLDB_COL_COPY_START },
|
||||
],
|
||||
})
|
||||
.component();
|
||||
this.disposables.push(
|
||||
this._columnSortDropdown.onValueChanged(
|
||||
async (value) => await this._populateTableData()));
|
||||
|
||||
this._columnSortCheckbox = this.view.modelBuilder.checkBox()
|
||||
.withProps({
|
||||
label: loc.ASCENDING_LABEL,
|
||||
checked: false,
|
||||
CSSStyles: { 'margin-left': '15px' },
|
||||
})
|
||||
.component();
|
||||
this.disposables.push(
|
||||
this._columnSortCheckbox.onChanged(
|
||||
async (value) => await this._populateTableData()));
|
||||
|
||||
const columnSortContainer = this.view.modelBuilder.flexContainer()
|
||||
.withItems([sortLabel, this._columnSortDropdown])
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'justify-content': 'left',
|
||||
'align-items': 'center',
|
||||
'padding': '0px',
|
||||
'display': 'flex',
|
||||
'flex-direction': 'row',
|
||||
},
|
||||
}).component();
|
||||
columnSortContainer.addItem(this._columnSortCheckbox, { flex: '0 0 auto' });
|
||||
|
||||
const flexContainer = this.view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
width: '100%',
|
||||
CSSStyles: {
|
||||
'justify-content': 'left',
|
||||
'align-items': 'center',
|
||||
'padding': '0px',
|
||||
'display': 'flex',
|
||||
'flex-direction': 'row',
|
||||
'flex-flow': 'wrap',
|
||||
},
|
||||
}).component();
|
||||
flexContainer.addItem(this._tableFilterInputBox, { flex: '0' });
|
||||
flexContainer.addItem(columnSortContainer, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
|
||||
|
||||
return flexContainer;
|
||||
}
|
||||
|
||||
private async _createStatusBar(): Promise<azdata.FlexContainer> {
|
||||
const serverObjectsLabel = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.SERVER_OBJECTS_LABEL,
|
||||
CSSStyles: {
|
||||
'font-weight': '600',
|
||||
'font-size': '14px',
|
||||
'margin': '0 0 5px 0',
|
||||
},
|
||||
})
|
||||
.component();
|
||||
|
||||
const flexContainer = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'row',
|
||||
flexWrap: 'wrap',
|
||||
})
|
||||
.component();
|
||||
|
||||
flexContainer.addItems([
|
||||
await this.createInfoCard(loc.SERVER_OBJECTS_ALL_TABLES_LABEL, IconPathHelper.allTables),
|
||||
await this.createInfoCard(loc.SERVER_OBJECTS_IN_PROGRESS_TABLES_LABEL, IconPathHelper.inProgressMigration),
|
||||
await this.createInfoCard(loc.SERVER_OBJECTS_SUCCESSFUL_TABLES_LABEL, IconPathHelper.completedMigration),
|
||||
await this.createInfoCard(loc.SERVER_OBJECTS_FAILED_TABLES_LABEL, IconPathHelper.error),
|
||||
await this.createInfoCard(loc.SERVER_OBJECTS_CANCELLED_TABLES_LABEL, IconPathHelper.cancel)
|
||||
], { flex: '0 0 auto', CSSStyles: { 'width': '168px' } });
|
||||
|
||||
return this.view.modelBuilder.flexContainer()
|
||||
.withItems([serverObjectsLabel, flexContainer])
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
}
|
||||
|
||||
protected async migrationInfoGrid(): Promise<azdata.FlexContainer> {
|
||||
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
|
||||
container.addItem(
|
||||
infoField.flexContainer,
|
||||
{ CSSStyles: { width: infoFieldLgWidth } });
|
||||
};
|
||||
|
||||
const flexServer = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
|
||||
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
|
||||
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
|
||||
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
|
||||
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
|
||||
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
|
||||
|
||||
const flexTarget = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
|
||||
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
|
||||
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
|
||||
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
|
||||
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
|
||||
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
|
||||
|
||||
const flexStatus = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
|
||||
this._serverObjectsInfoField = await this.createInfoField(loc.SERVER_OBJECTS_FIELD_LABEL, '');
|
||||
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
|
||||
addInfoFieldToContainer(this._serverObjectsInfoField, flexStatus);
|
||||
|
||||
const flexInfoProps = {
|
||||
flex: '0',
|
||||
CSSStyles: { 'flex': '0', 'width': infoFieldLgWidth }
|
||||
};
|
||||
|
||||
const flexInfo = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexWrap: 'wrap' })
|
||||
.withProps({ width: '100%' })
|
||||
.component();
|
||||
flexInfo.addItem(flexServer, flexInfoProps);
|
||||
flexInfo.addItem(flexTarget, flexInfoProps);
|
||||
flexInfo.addItem(flexStatus, flexInfoProps);
|
||||
|
||||
return flexInfo;
|
||||
}
|
||||
}
|
||||
771
extensions/sql-migration/src/dashboard/migrationsListTab.ts
Normal file
771
extensions/sql-migration/src/dashboard/migrationsListTab.ts
Normal file
@@ -0,0 +1,771 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { getCurrentMigrations, getSelectedServiceStatus, MigrationLocalStorage } from '../models/migrationLocalStorage';
|
||||
import * as loc from '../constants/strings';
|
||||
import { filterMigrations, getMigrationDuration, getMigrationStatusImage, getMigrationStatusWithErrors, getMigrationTime } from '../api/utils';
|
||||
import { SqlMigrationServiceDetailsDialog } from '../dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog';
|
||||
import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog';
|
||||
import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migrationCutoverDialogModel';
|
||||
import { getMigrationTargetType, getMigrationMode, canRetryMigration, getMigrationModeEnum, canCancelMigration, canCutoverMigration, getMigrationStatus } from '../constants/helper';
|
||||
import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
|
||||
import { DatabaseMigration, getResourceName } from '../api/azure';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
|
||||
import { AdsMigrationStatus, EmptySettingValue, MenuCommands, TabBase } from './tabBase';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
import { MigrationMode } from '../models/stateMachine';
|
||||
|
||||
export const MigrationsListTabId = 'MigrationsListTab';
|
||||
|
||||
const TableColumns = {
|
||||
sourceDatabase: 'sourceDatabase',
|
||||
sourceServer: 'sourceServer',
|
||||
status: 'status',
|
||||
mode: 'mode',
|
||||
targetType: 'targetType',
|
||||
targetDatabse: 'targetDatabase',
|
||||
targetServer: 'TargetServer',
|
||||
duration: 'duration',
|
||||
startTime: 'startTime',
|
||||
finishTime: 'finishTime',
|
||||
};
|
||||
|
||||
export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
private _searchBox!: azdata.InputBoxComponent;
|
||||
private _refresh!: azdata.ButtonComponent;
|
||||
private _serviceContextButton!: azdata.ButtonComponent;
|
||||
private _statusDropdown!: azdata.DropDownComponent;
|
||||
private _columnSortDropdown!: azdata.DropDownComponent;
|
||||
private _columnSortCheckbox!: azdata.CheckBoxComponent;
|
||||
private _statusTable!: azdata.TableComponent;
|
||||
private _refreshLoader!: azdata.LoadingComponent;
|
||||
private _filteredMigrations: DatabaseMigration[] = [];
|
||||
private _openMigrationDetails!: (migration: DatabaseMigration) => Promise<void>;
|
||||
private _migrations: DatabaseMigration[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.id = MigrationsListTabId;
|
||||
}
|
||||
|
||||
public async create(
|
||||
context: vscode.ExtensionContext,
|
||||
view: azdata.ModelView,
|
||||
openMigrationDetails: (migration: DatabaseMigration) => Promise<void>,
|
||||
statusBar: DashboardStatusBar,
|
||||
): Promise<MigrationsListTab> {
|
||||
|
||||
this.view = view;
|
||||
this.context = context;
|
||||
this._openMigrationDetails = openMigrationDetails;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public onDialogClosed = async (): Promise<void> =>
|
||||
await this.updateServiceContext(this._serviceContextButton);
|
||||
|
||||
public async setMigrationFilter(filter: AdsMigrationStatus): Promise<void> {
|
||||
if (this._statusDropdown.values && this._statusDropdown.values.length > 0) {
|
||||
const statusFilter = (<azdata.CategoryValue[]>this._statusDropdown.values)
|
||||
.find(value => value.name === filter.toString());
|
||||
|
||||
this._statusDropdown.value = statusFilter;
|
||||
}
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
if (this.isRefreshing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this._refresh.enabled = false;
|
||||
this._refreshLoader.loading = true;
|
||||
await this.statusBar.clearError();
|
||||
|
||||
try {
|
||||
await this._statusTable.updateProperty('data', []);
|
||||
this._migrations = await getCurrentMigrations();
|
||||
await this._populateMigrationTable();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.DASHBOARD_REFRESH_MIGRATIONS_TITLE,
|
||||
loc.DASHBOARD_REFRESH_MIGRATIONS_LABEL,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, 'refreshMigrations', e);
|
||||
} finally {
|
||||
this._refreshLoader.loading = false;
|
||||
this._refresh.enabled = true;
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected async initialize(): Promise<void> {
|
||||
this._registerCommands();
|
||||
|
||||
this.content = this.view.modelBuilder.flexContainer()
|
||||
.withItems(
|
||||
[
|
||||
this._createToolbar(),
|
||||
await this._createSearchAndSortContainer(),
|
||||
this._createStatusTable()
|
||||
],
|
||||
{ CSSStyles: { 'width': '100%' } }
|
||||
).withLayout({
|
||||
width: '100%',
|
||||
flexFlow: 'column',
|
||||
}).withProps({ CSSStyles: { 'padding': '0px' } })
|
||||
.component();
|
||||
}
|
||||
|
||||
private _createToolbar(): azdata.ToolbarContainer {
|
||||
const toolbar = this.view.modelBuilder.toolbarContainer();
|
||||
|
||||
this._refresh = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.refresh,
|
||||
iconHeight: 24,
|
||||
iconWidth: 24,
|
||||
height: 24,
|
||||
label: loc.REFRESH_BUTTON_LABEL,
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
this._refresh.onDidClick(
|
||||
async (e) => await this.refresh()));
|
||||
|
||||
this._refreshLoader = this.view.modelBuilder.loadingComponent()
|
||||
.withProps({
|
||||
loading: false,
|
||||
CSSStyles: {
|
||||
'height': '8px',
|
||||
'margin-top': '6px'
|
||||
}
|
||||
})
|
||||
.component();
|
||||
|
||||
toolbar.addToolbarItems([
|
||||
<azdata.ToolbarComponent>{ component: this.createNewMigrationButton(), toolbarSeparatorAfter: true },
|
||||
<azdata.ToolbarComponent>{ component: this.createNewSupportRequestButton() },
|
||||
<azdata.ToolbarComponent>{ component: this.createFeedbackButton(), toolbarSeparatorAfter: true },
|
||||
<azdata.ToolbarComponent>{ component: this._refresh },
|
||||
<azdata.ToolbarComponent>{ component: this._refreshLoader },
|
||||
]);
|
||||
|
||||
return toolbar.component();
|
||||
}
|
||||
|
||||
private async _createSearchAndSortContainer(): Promise<azdata.FlexContainer> {
|
||||
const serviceContextLabel = await getSelectedServiceStatus();
|
||||
this._serviceContextButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.sqlMigrationService,
|
||||
iconHeight: 22,
|
||||
iconWidth: 22,
|
||||
label: serviceContextLabel,
|
||||
title: serviceContextLabel,
|
||||
description: loc.MIGRATION_SERVICE_DESCRIPTION,
|
||||
buttonType: azdata.ButtonType.Informational,
|
||||
width: 230,
|
||||
}).component();
|
||||
|
||||
const onDialogClosed = async (): Promise<void> =>
|
||||
await this.updateServiceContext(this._serviceContextButton);
|
||||
|
||||
this.disposables.push(
|
||||
this._serviceContextButton.onDidClick(
|
||||
async () => {
|
||||
const dialog = new SelectMigrationServiceDialog(onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
|
||||
this._searchBox = this.view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
stopEnterPropagation: true,
|
||||
placeHolder: loc.SEARCH_FOR_MIGRATIONS,
|
||||
width: '200px',
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
this._searchBox.onTextChanged(
|
||||
async (value) => await this._populateMigrationTable()));
|
||||
|
||||
const searchLabel = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.STATUS_LABEL,
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'font-weight': '600',
|
||||
'margin': '3px 0 0 0',
|
||||
},
|
||||
}).component();
|
||||
|
||||
this._statusDropdown = this.view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
ariaLabel: loc.MIGRATION_STATUS_FILTER,
|
||||
values: this._statusDropdownValues,
|
||||
width: '150px'
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
this._statusDropdown.onValueChanged(
|
||||
async (value) => await this._populateMigrationTable()));
|
||||
|
||||
const searchContainer = this.view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
}).withProps({ CSSStyles: { 'margin-left': '10px' } })
|
||||
.component();
|
||||
searchContainer.addItem(searchLabel, { flex: '0' });
|
||||
searchContainer.addItem(this._statusDropdown, { flex: '0', CSSStyles: { 'margin-left': '5px' } });
|
||||
|
||||
const sortLabel = this.view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.SORT_LABEL,
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'font-weight': '600',
|
||||
'margin': '3px 0 0 0',
|
||||
},
|
||||
}).component();
|
||||
|
||||
this._columnSortDropdown = this.view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
editable: false,
|
||||
width: 120,
|
||||
CSSStyles: { 'margin-left': '5px' },
|
||||
value: <azdata.CategoryValue>{ name: TableColumns.startTime, displayName: loc.START_TIME },
|
||||
values: [
|
||||
<azdata.CategoryValue>{ name: TableColumns.sourceDatabase, displayName: loc.SRC_DATABASE },
|
||||
<azdata.CategoryValue>{ name: TableColumns.sourceServer, displayName: loc.SRC_SERVER },
|
||||
<azdata.CategoryValue>{ name: TableColumns.status, displayName: loc.STATUS_COLUMN },
|
||||
<azdata.CategoryValue>{ name: TableColumns.mode, displayName: loc.MIGRATION_MODE },
|
||||
<azdata.CategoryValue>{ name: TableColumns.targetType, displayName: loc.AZURE_SQL_TARGET },
|
||||
<azdata.CategoryValue>{ name: TableColumns.targetDatabse, displayName: loc.TARGET_DATABASE_COLUMN },
|
||||
<azdata.CategoryValue>{ name: TableColumns.targetServer, displayName: loc.TARGET_SERVER_COLUMN },
|
||||
<azdata.CategoryValue>{ name: TableColumns.duration, displayName: loc.DURATION },
|
||||
<azdata.CategoryValue>{ name: TableColumns.startTime, displayName: loc.START_TIME },
|
||||
<azdata.CategoryValue>{ name: TableColumns.finishTime, displayName: loc.FINISH_TIME },
|
||||
],
|
||||
})
|
||||
.component();
|
||||
this.disposables.push(
|
||||
this._columnSortDropdown.onValueChanged(
|
||||
async (e) => await this._populateMigrationTable()));
|
||||
|
||||
this._columnSortCheckbox = this.view.modelBuilder.checkBox()
|
||||
.withProps({
|
||||
label: loc.ASCENDING_LABEL,
|
||||
checked: false,
|
||||
CSSStyles: { 'margin-left': '15px' },
|
||||
})
|
||||
.component();
|
||||
this.disposables.push(
|
||||
this._columnSortCheckbox.onChanged(
|
||||
async (e) => await this._populateMigrationTable()));
|
||||
|
||||
const columnSortContainer = this.view.modelBuilder.flexContainer()
|
||||
.withItems([sortLabel, this._columnSortDropdown])
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'justify-content': 'left',
|
||||
'align-items': 'center',
|
||||
'padding': '0px',
|
||||
'display': 'flex',
|
||||
'flex-direction': 'row',
|
||||
},
|
||||
}).component();
|
||||
columnSortContainer.addItem(this._columnSortCheckbox, { flex: '0 0 auto' });
|
||||
|
||||
const flexContainer = this.view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
width: '100%',
|
||||
CSSStyles: {
|
||||
'justify-content': 'left',
|
||||
'align-items': 'center',
|
||||
'padding': '0px',
|
||||
'display': 'flex',
|
||||
'flex-direction': 'row',
|
||||
'flex-flow': 'wrap',
|
||||
},
|
||||
}).component();
|
||||
|
||||
flexContainer.addItem(this._serviceContextButton, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
|
||||
flexContainer.addItem(this._searchBox, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
|
||||
flexContainer.addItem(searchContainer, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
|
||||
flexContainer.addItem(columnSortContainer, { flex: '0', CSSStyles: { 'margin-left': '10px' } });
|
||||
|
||||
const container = this.view.modelBuilder.flexContainer()
|
||||
.withProps({ width: '100%' })
|
||||
.component();
|
||||
|
||||
container.addItem(flexContainer);
|
||||
return container;
|
||||
}
|
||||
|
||||
private _registerCommands(): void {
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.Cutover,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(
|
||||
migration => migration.id === migrationId);
|
||||
|
||||
if (canRetryMigration(migration)) {
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
const dialog = new ConfirmCutoverDialog(cutoverDialogModel);
|
||||
await dialog.initialize();
|
||||
if (cutoverDialogModel.CutoverError) {
|
||||
void vscode.window.showErrorMessage(loc.MIGRATION_CUTOVER_ERROR);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, cutoverDialogModel.CutoverError);
|
||||
}
|
||||
} else {
|
||||
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CUTOVER);
|
||||
}
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_CUTOVER_ERROR,
|
||||
loc.MIGRATION_CUTOVER_ERROR,
|
||||
e.message);
|
||||
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.ViewDatabase,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(m => m.id === migrationId);
|
||||
await this._openMigrationDetails(migration!);
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.OPEN_MIGRATION_DETAILS_ERROR,
|
||||
loc.OPEN_MIGRATION_DETAILS_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewDatabase, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.ViewTarget,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
const url = 'https://portal.azure.com/#resource/' + migration!.properties.scope;
|
||||
await vscode.env.openExternal(vscode.Uri.parse(url));
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.OPEN_MIGRATION_TARGET_ERROR,
|
||||
loc.OPEN_MIGRATION_TARGET_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewTarget, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.ViewService,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
const dialog = new SqlMigrationServiceDetailsDialog(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await dialog.initialize();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.OPEN_MIGRATION_SERVICE_ERROR,
|
||||
loc.OPEN_MIGRATION_SERVICE_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewService, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.CopyMigration,
|
||||
async (migrationId: string) => {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
|
||||
try {
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.CopyMigration, e);
|
||||
}
|
||||
|
||||
await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migration, undefined, 2));
|
||||
await vscode.window.showInformationMessage(loc.DETAILS_COPIED);
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.CancelMigration,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
if (canCancelMigration(migration)) {
|
||||
void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => {
|
||||
if (v === loc.YES) {
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
await cutoverDialogModel.cancelMigration();
|
||||
|
||||
if (cutoverDialogModel.CancelMigrationError) {
|
||||
void vscode.window.showErrorMessage(loc.MIGRATION_CANNOT_CANCEL);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, cutoverDialogModel.CancelMigrationError);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CANCEL);
|
||||
}
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_CANCELLATION_ERROR,
|
||||
loc.MIGRATION_CANCELLATION_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.RetryMigration,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
if (canRetryMigration(migration)) {
|
||||
let retryMigrationDialog = new RetryMigrationDialog(
|
||||
this.context,
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!,
|
||||
async () => await this.onDialogClosed());
|
||||
await retryMigrationDialog.openDialog();
|
||||
}
|
||||
else {
|
||||
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_RETRY);
|
||||
}
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_RETRY_ERROR,
|
||||
loc.MIGRATION_RETRY_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.RetryMigration, e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _sortMigrations(migrations: DatabaseMigration[], columnName: string, ascending: boolean): void {
|
||||
const sortDir = ascending ? -1 : 1;
|
||||
switch (columnName) {
|
||||
case TableColumns.sourceDatabase:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.stringCompare(
|
||||
m1.properties.sourceDatabaseName,
|
||||
m2.properties.sourceDatabaseName,
|
||||
sortDir));
|
||||
return;
|
||||
case TableColumns.sourceServer:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.stringCompare(
|
||||
m1.properties.sourceServerName,
|
||||
m2.properties.sourceServerName,
|
||||
sortDir));
|
||||
return;
|
||||
case TableColumns.status:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.stringCompare(
|
||||
getMigrationStatusWithErrors(m1),
|
||||
getMigrationStatusWithErrors(m2),
|
||||
sortDir));
|
||||
return;
|
||||
case TableColumns.mode:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.stringCompare(
|
||||
getMigrationMode(m1),
|
||||
getMigrationMode(m2),
|
||||
sortDir));
|
||||
return;
|
||||
case TableColumns.targetType:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.stringCompare(
|
||||
getMigrationTargetType(m1),
|
||||
getMigrationTargetType(m2),
|
||||
sortDir));
|
||||
return;
|
||||
case TableColumns.targetDatabse:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.stringCompare(
|
||||
getResourceName(m1.id),
|
||||
getResourceName(m2.id),
|
||||
sortDir));
|
||||
return;
|
||||
case TableColumns.targetServer:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.stringCompare(
|
||||
getResourceName(m1.properties.scope),
|
||||
getResourceName(m2.properties.scope),
|
||||
sortDir));
|
||||
return;
|
||||
case TableColumns.duration:
|
||||
migrations.sort((m1, m2) => {
|
||||
if (!m1.properties.startedOn) {
|
||||
return sortDir;
|
||||
} else if (!m2.properties.startedOn) {
|
||||
return -sortDir;
|
||||
}
|
||||
const m1_startedOn = new Date(m1.properties.startedOn);
|
||||
const m2_startedOn = new Date(m2.properties.startedOn);
|
||||
const m1_endedOn = new Date(m1.properties.endedOn ?? Date.now());
|
||||
const m2_endedOn = new Date(m2.properties.endedOn ?? Date.now());
|
||||
const m1_duration = m1_endedOn.getTime() - m1_startedOn.getTime();
|
||||
const m2_duration = m2_endedOn.getTime() - m2_startedOn.getTime();
|
||||
return m1_duration > m2_duration ? -sortDir : sortDir;
|
||||
});
|
||||
return;
|
||||
case TableColumns.startTime:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.dateCompare(
|
||||
m1.properties.startedOn,
|
||||
m2.properties.startedOn,
|
||||
sortDir));
|
||||
return;
|
||||
case TableColumns.finishTime:
|
||||
migrations.sort(
|
||||
(m1, m2) => this.dateCompare(
|
||||
m1.properties.endedOn,
|
||||
m2.properties.endedOn,
|
||||
sortDir));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async _populateMigrationTable(): Promise<void> {
|
||||
try {
|
||||
this._filteredMigrations = filterMigrations(
|
||||
this._migrations,
|
||||
(<azdata.CategoryValue>this._statusDropdown.value).name,
|
||||
this._searchBox.value!);
|
||||
|
||||
this._sortMigrations(
|
||||
this._filteredMigrations,
|
||||
(<azdata.CategoryValue>this._columnSortDropdown.value).name,
|
||||
this._columnSortCheckbox.checked === true);
|
||||
|
||||
const data: any[] = this._filteredMigrations.map((migration, index) => {
|
||||
return [
|
||||
<azdata.HyperlinkColumnCellValue>{
|
||||
icon: IconPathHelper.sqlDatabaseLogo,
|
||||
title: migration.properties.sourceDatabaseName ?? EmptySettingValue,
|
||||
}, // sourceDatabase
|
||||
migration.properties.sourceServerName ?? EmptySettingValue, // sourceServer
|
||||
<azdata.HyperlinkColumnCellValue>{
|
||||
icon: getMigrationStatusImage(migration),
|
||||
title: getMigrationStatusWithErrors(migration),
|
||||
}, // statue
|
||||
getMigrationMode(migration), // mode
|
||||
getMigrationTargetType(migration), // targetType
|
||||
getResourceName(migration.id), // targetDatabase
|
||||
getResourceName(migration.properties.scope), // targetServer
|
||||
getMigrationDuration(
|
||||
migration.properties.startedOn,
|
||||
migration.properties.endedOn), // duration
|
||||
getMigrationTime(migration.properties.startedOn), // startTime
|
||||
getMigrationTime(migration.properties.endedOn), // finishTime
|
||||
<azdata.ContextMenuColumnCellValue>{
|
||||
title: '',
|
||||
context: migration.id,
|
||||
commands: this._getMenuCommands(migration), // context menu
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
await this._statusTable.updateProperty('data', data);
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.LOAD_MIGRATION_LIST_ERROR,
|
||||
loc.LOAD_MIGRATION_LIST_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationStatusDialog, 'Error populating migrations list page', e);
|
||||
}
|
||||
}
|
||||
|
||||
private _createStatusTable(): azdata.TableComponent {
|
||||
const headerCssStyles = undefined;
|
||||
const rowCssStyles = undefined;
|
||||
|
||||
this._statusTable = this.view.modelBuilder.table()
|
||||
.withProps({
|
||||
ariaLabel: loc.MIGRATION_STATUS,
|
||||
CSSStyles: { 'margin-left': '10px' },
|
||||
data: [],
|
||||
forceFitColumns: azdata.ColumnSizingMode.AutoFit,
|
||||
height: '500px',
|
||||
columns: [
|
||||
<azdata.HyperlinkColumn>{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.SRC_DATABASE,
|
||||
value: 'sourceDatabase',
|
||||
width: 190,
|
||||
type: azdata.ColumnType.hyperlink,
|
||||
showText: true,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.SRC_SERVER,
|
||||
value: 'sourceServer',
|
||||
width: 190,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
<azdata.HyperlinkColumn>{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.STATUS_COLUMN,
|
||||
value: 'status',
|
||||
width: 120,
|
||||
type: azdata.ColumnType.hyperlink,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.MIGRATION_MODE,
|
||||
value: 'mode',
|
||||
width: 55,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.AZURE_SQL_TARGET,
|
||||
value: 'targetType',
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.TARGET_DATABASE_COLUMN,
|
||||
value: 'targetDatabase',
|
||||
width: 125,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.TARGET_SERVER_COLUMN,
|
||||
value: 'targetServer',
|
||||
width: 125,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.DURATION,
|
||||
value: 'duration',
|
||||
width: 55,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.START_TIME,
|
||||
value: 'startTime',
|
||||
width: 115,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.FINISH_TIME,
|
||||
value: 'finishTime',
|
||||
width: 115,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: '',
|
||||
value: 'contextMenu',
|
||||
width: 25,
|
||||
type: azdata.ColumnType.contextMenu,
|
||||
}
|
||||
]
|
||||
}).component();
|
||||
|
||||
this.disposables.push(this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => {
|
||||
const buttonState = <azdata.ICellActionEventArgs>rowState;
|
||||
const migration = this._filteredMigrations[rowState.row];
|
||||
switch (buttonState?.column) {
|
||||
case 2:
|
||||
const status = getMigrationStatus(migration);
|
||||
const statusMessage = loc.DATABASE_MIGRATION_STATUS_LABEL(status);
|
||||
const errors = this.getMigrationErrors(migration!);
|
||||
|
||||
this.showDialogMessage(
|
||||
loc.DATABASE_MIGRATION_STATUS_TITLE,
|
||||
statusMessage,
|
||||
errors);
|
||||
break;
|
||||
case 0:
|
||||
await this._openMigrationDetails(migration);
|
||||
break;
|
||||
}
|
||||
}));
|
||||
|
||||
return this._statusTable;
|
||||
}
|
||||
|
||||
private _getMenuCommands(migration: DatabaseMigration): string[] {
|
||||
const menuCommands: string[] = [];
|
||||
|
||||
if (getMigrationModeEnum(migration) === MigrationMode.ONLINE &&
|
||||
canCutoverMigration(migration)) {
|
||||
menuCommands.push(MenuCommands.Cutover);
|
||||
}
|
||||
|
||||
menuCommands.push(...[
|
||||
MenuCommands.ViewDatabase,
|
||||
MenuCommands.ViewTarget,
|
||||
MenuCommands.ViewService,
|
||||
MenuCommands.CopyMigration]);
|
||||
|
||||
if (canCancelMigration(migration)) {
|
||||
menuCommands.push(MenuCommands.CancelMigration);
|
||||
}
|
||||
|
||||
return menuCommands;
|
||||
}
|
||||
|
||||
private _statusDropdownValues: azdata.CategoryValue[] = [
|
||||
{ displayName: loc.STATUS_ALL, name: AdsMigrationStatus.ALL },
|
||||
{ displayName: loc.STATUS_ONGOING, name: AdsMigrationStatus.ONGOING },
|
||||
{ displayName: loc.STATUS_COMPLETING, name: AdsMigrationStatus.COMPLETING },
|
||||
{ displayName: loc.STATUS_SUCCEEDED, name: AdsMigrationStatus.SUCCEEDED },
|
||||
{ displayName: loc.STATUS_FAILED, name: AdsMigrationStatus.FAILED }
|
||||
];
|
||||
}
|
||||
148
extensions/sql-migration/src/dashboard/migrationsTab.ts
Normal file
148
extensions/sql-migration/src/dashboard/migrationsTab.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as loc from '../constants/strings';
|
||||
import { AdsMigrationStatus, TabBase } from './tabBase';
|
||||
import { MigrationsListTab, MigrationsListTabId } from './migrationsListTab';
|
||||
import { DatabaseMigration } from '../api/azure';
|
||||
import { MigrationLocalStorage } from '../models/migrationLocalStorage';
|
||||
import { FileStorageType } from '../models/stateMachine';
|
||||
import { MigrationDetailsTabBase } from './migrationDetailsTabBase';
|
||||
import { MigrationDetailsFileShareTab } from './migrationDetailsFileShareTab';
|
||||
import { MigrationDetailsBlobContainerTab } from './migrationDetailsBlobContainerTab';
|
||||
import { MigrationDetailsTableTab } from './migrationDetailsTableTab';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
|
||||
export const MigrationsTabId = 'MigrationsTab';
|
||||
|
||||
export class MigrationsTab extends TabBase<MigrationsTab> {
|
||||
private _tab!: azdata.DivContainer;
|
||||
private _migrationsListTab!: MigrationsListTab;
|
||||
private _migrationDetailsTab!: MigrationDetailsTabBase<any>;
|
||||
private _migrationDetailsFileShareTab!: MigrationDetailsTabBase<any>;
|
||||
private _migrationDetailsBlobTab!: MigrationDetailsTabBase<any>;
|
||||
private _migrationDetailsTableTab!: MigrationDetailsTabBase<any>;
|
||||
private _selectedTabId: string | undefined = undefined;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.title = loc.DESKTOP_MIGRATIONS_TAB_TITLE;
|
||||
this.id = MigrationsTabId;
|
||||
}
|
||||
|
||||
public onDialogClosed = async (): Promise<void> =>
|
||||
await this._migrationsListTab.onDialogClosed();
|
||||
|
||||
public async create(
|
||||
context: vscode.ExtensionContext,
|
||||
view: azdata.ModelView,
|
||||
statusBar: DashboardStatusBar): Promise<MigrationsTab> {
|
||||
|
||||
this.context = context;
|
||||
this.view = view;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize(view);
|
||||
await this._openTab(this._migrationsListTab);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
switch (this._selectedTabId) {
|
||||
case undefined:
|
||||
case MigrationsListTabId:
|
||||
return await this._migrationsListTab?.refresh();
|
||||
default:
|
||||
return await this._migrationDetailsTab?.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
protected async initialize(view: azdata.ModelView): Promise<void> {
|
||||
this._tab = this.view.modelBuilder.divContainer()
|
||||
.withLayout({ height: '100%' })
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'margin': '0px',
|
||||
'padding': '0px',
|
||||
'width': '100%'
|
||||
}
|
||||
})
|
||||
.component();
|
||||
|
||||
this._migrationsListTab = await new MigrationsListTab().create(
|
||||
this.context,
|
||||
this.view,
|
||||
async (migration) => await this._openMigrationDetails(migration),
|
||||
this.statusBar);
|
||||
this.disposables.push(this._migrationsListTab);
|
||||
|
||||
this._migrationDetailsBlobTab = await new MigrationDetailsBlobContainerTab().create(
|
||||
this.context,
|
||||
this.view,
|
||||
async () => await this._openMigrationsListTab(),
|
||||
this.statusBar);
|
||||
this.disposables.push(this._migrationDetailsBlobTab);
|
||||
|
||||
this._migrationDetailsFileShareTab = await new MigrationDetailsFileShareTab().create(
|
||||
this.context,
|
||||
this.view,
|
||||
async () => await this._openMigrationsListTab(),
|
||||
this.statusBar);
|
||||
this.disposables.push(this._migrationDetailsFileShareTab);
|
||||
|
||||
this._migrationDetailsTableTab = await new MigrationDetailsTableTab().create(
|
||||
this.context,
|
||||
this.view,
|
||||
async () => await this._openMigrationsListTab(),
|
||||
this.statusBar);
|
||||
this.disposables.push(this._migrationDetailsFileShareTab);
|
||||
|
||||
this.content = this._tab;
|
||||
}
|
||||
|
||||
public async setMigrationFilter(filter: AdsMigrationStatus): Promise<void> {
|
||||
await this._migrationsListTab?.setMigrationFilter(filter);
|
||||
await this._openTab(this._migrationsListTab);
|
||||
await this._migrationsListTab?.setMigrationFilter(filter);
|
||||
}
|
||||
|
||||
private async _openMigrationDetails(migration: DatabaseMigration): Promise<void> {
|
||||
switch (migration.properties.backupConfiguration?.sourceLocation?.fileStorageType) {
|
||||
case FileStorageType.AzureBlob:
|
||||
this._migrationDetailsTab = this._migrationDetailsBlobTab;
|
||||
break;
|
||||
case FileStorageType.FileShare:
|
||||
this._migrationDetailsTab = this._migrationDetailsFileShareTab;
|
||||
break;
|
||||
case FileStorageType.None:
|
||||
this._migrationDetailsTab = this._migrationDetailsTableTab;
|
||||
break;
|
||||
}
|
||||
|
||||
await this._migrationDetailsTab.setMigrationContext(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration);
|
||||
|
||||
await this._openTab(this._migrationDetailsTab);
|
||||
}
|
||||
|
||||
private async _openMigrationsListTab(): Promise<void> {
|
||||
await this.statusBar.clearError();
|
||||
await this._openTab(this._migrationsListTab);
|
||||
}
|
||||
|
||||
private async _openTab(tab: azdata.Tab): Promise<void> {
|
||||
if (tab.id === this._selectedTabId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._tab.clearItems();
|
||||
this._tab.addItem(tab.content);
|
||||
this._selectedTabId = tab.id;
|
||||
}
|
||||
}
|
||||
@@ -5,793 +5,193 @@
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import * as loc from '../constants/strings';
|
||||
import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { MigrationStatusDialog } from '../dialog/migrationStatus/migrationStatusDialog';
|
||||
import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel';
|
||||
import { filterMigrations } from '../api/utils';
|
||||
import * as styles from '../constants/styles';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
|
||||
import { DatabaseMigration } from '../api/azure';
|
||||
import { getCurrentMigrations, getSelectedServiceStatus, isServiceContextValid, MigrationLocalStorage } from '../models/migrationLocalStorage';
|
||||
const localize = nls.loadMessageBundle();
|
||||
import { DashboardTab } from './dashboardTab';
|
||||
import { MigrationsTab, MigrationsTabId } from './migrationsTab';
|
||||
import { AdsMigrationStatus } from './tabBase';
|
||||
|
||||
interface IActionMetadata {
|
||||
title?: string,
|
||||
description?: string,
|
||||
link?: string,
|
||||
iconPath?: azdata.ThemedIconPath,
|
||||
command?: string;
|
||||
export interface DashboardStatusBar {
|
||||
showError: (errorTitle: string, errorLable: string, errorDescription: string) => Promise<void>;
|
||||
clearError: () => Promise<void>;
|
||||
errorTitle: string;
|
||||
errorLabel: string;
|
||||
errorDescription: string;
|
||||
}
|
||||
|
||||
const maxWidth = 800;
|
||||
const BUTTON_CSS = {
|
||||
'font-size': '13px',
|
||||
'line-height': '18px',
|
||||
'margin': '4px 0',
|
||||
'text-align': 'left',
|
||||
};
|
||||
|
||||
interface StatusCard {
|
||||
container: azdata.DivContainer;
|
||||
count: azdata.TextComponent,
|
||||
textContainer?: azdata.FlexContainer,
|
||||
warningContainer?: azdata.FlexContainer,
|
||||
warningText?: azdata.TextComponent,
|
||||
}
|
||||
|
||||
export class DashboardWidget {
|
||||
export class DashboardWidget implements DashboardStatusBar {
|
||||
private _context: vscode.ExtensionContext;
|
||||
private _migrationStatusCardsContainer!: azdata.FlexContainer;
|
||||
private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent;
|
||||
private _view!: azdata.ModelView;
|
||||
private _inProgressMigrationButton!: StatusCard;
|
||||
private _inProgressWarningMigrationButton!: StatusCard;
|
||||
private _allMigrationButton!: StatusCard;
|
||||
private _successfulMigrationButton!: StatusCard;
|
||||
private _failedMigrationButton!: StatusCard;
|
||||
private _completingMigrationButton!: StatusCard;
|
||||
private _selectServiceText!: azdata.TextComponent;
|
||||
private _serviceContextButton!: azdata.ButtonComponent;
|
||||
private _refreshButton!: azdata.ButtonComponent;
|
||||
|
||||
private _tabs!: azdata.TabbedPanelComponent;
|
||||
private _statusInfoBox!: azdata.InfoBoxComponent;
|
||||
private _dashboardTab!: DashboardTab;
|
||||
private _migrationsTab!: MigrationsTab;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
private isRefreshing: boolean = false;
|
||||
|
||||
public onDialogClosed = async (): Promise<void> => {
|
||||
const label = await getSelectedServiceStatus();
|
||||
this._serviceContextButton.label = label;
|
||||
this._serviceContextButton.title = label;
|
||||
await this.refreshMigrations();
|
||||
};
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
this._context = context;
|
||||
}
|
||||
|
||||
public errorTitle: string = '';
|
||||
public errorLabel: string = '';
|
||||
public errorDescription: string = '';
|
||||
|
||||
public async showError(errorTitle: string, errorLabel: string, errorDescription: string): Promise<void> {
|
||||
this.errorTitle = errorTitle;
|
||||
this.errorLabel = errorLabel;
|
||||
this.errorDescription = errorDescription;
|
||||
this._statusInfoBox.style = 'error';
|
||||
this._statusInfoBox.text = errorTitle;
|
||||
await this._updateStatusDisplay(this._statusInfoBox, true);
|
||||
}
|
||||
|
||||
public async clearError(): Promise<void> {
|
||||
await this._updateStatusDisplay(this._statusInfoBox, false);
|
||||
this.errorTitle = '';
|
||||
this.errorLabel = '';
|
||||
this.errorDescription = '';
|
||||
this._statusInfoBox.style = 'success';
|
||||
this._statusInfoBox.text = '';
|
||||
}
|
||||
|
||||
public register(): void {
|
||||
azdata.ui.registerModelViewProvider('migration.dashboard', async (view) => {
|
||||
azdata.ui.registerModelViewProvider('migration-dashboard', async (view) => {
|
||||
this._view = view;
|
||||
|
||||
const container = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}).component();
|
||||
|
||||
const header = this.createHeader(view);
|
||||
// Files need to have the vscode-file scheme to be loaded by ADS
|
||||
const watermarkUri = vscode.Uri
|
||||
.file(<string>IconPathHelper.migrationDashboardHeaderBackground.light)
|
||||
.with({ scheme: 'vscode-file' });
|
||||
|
||||
container.addItem(header, {
|
||||
CSSStyles: {
|
||||
'background-image': `
|
||||
url(${watermarkUri}),
|
||||
linear-gradient(0deg, rgba(0, 0, 0, 0.05) 0%, rgba(0, 0, 0, 0) 100%)
|
||||
`,
|
||||
'background-repeat': 'no-repeat',
|
||||
'background-position': '91.06% 100%',
|
||||
'margin-bottom': '20px'
|
||||
}
|
||||
});
|
||||
|
||||
const tasksContainer = await this.createTasks(view);
|
||||
header.addItem(tasksContainer, {
|
||||
CSSStyles: {
|
||||
'width': `${maxWidth}px`,
|
||||
'margin': '24px'
|
||||
}
|
||||
});
|
||||
container.addItem(await this.createFooter(view), {
|
||||
CSSStyles: {
|
||||
'margin': '0 24px'
|
||||
}
|
||||
});
|
||||
this._disposables.push(
|
||||
this._view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
|
||||
await view.initializeModel(container);
|
||||
await this.refreshMigrations();
|
||||
const openMigrationFcn = async (filter: AdsMigrationStatus): Promise<void> => {
|
||||
this._tabs.selectTab(MigrationsTabId);
|
||||
await this._migrationsTab.setMigrationFilter(filter);
|
||||
};
|
||||
|
||||
this._dashboardTab = await new DashboardTab().create(
|
||||
view,
|
||||
async (filter: AdsMigrationStatus) => await openMigrationFcn(filter),
|
||||
this);
|
||||
this._disposables.push(this._dashboardTab);
|
||||
|
||||
this._migrationsTab = await new MigrationsTab().create(
|
||||
this._context,
|
||||
view,
|
||||
this);
|
||||
this._disposables.push(this._migrationsTab);
|
||||
|
||||
this._tabs = view.modelBuilder.tabbedPanel()
|
||||
.withTabs([this._dashboardTab, this._migrationsTab])
|
||||
.withLayout({ alwaysShowTabs: true, orientation: azdata.TabOrientation.Horizontal })
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'margin': '0px',
|
||||
'padding': '0px',
|
||||
'width': '100%'
|
||||
}
|
||||
})
|
||||
.component();
|
||||
|
||||
this._disposables.push(
|
||||
this._tabs.onTabChanged(
|
||||
async id => {
|
||||
await this.clearError();
|
||||
await this.onDialogClosed();
|
||||
}));
|
||||
|
||||
this._statusInfoBox = view.modelBuilder.infoBox()
|
||||
.withProps({
|
||||
style: 'error',
|
||||
text: '',
|
||||
announceText: true,
|
||||
isClickable: true,
|
||||
display: 'none',
|
||||
CSSStyles: { 'font-size': '14px' },
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
this._statusInfoBox.onDidClick(
|
||||
async e => await this.openErrorDialog()));
|
||||
|
||||
const flexContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.withItems([this._statusInfoBox, this._tabs])
|
||||
.component();
|
||||
await view.initializeModel(flexContainer);
|
||||
|
||||
await this.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
private createHeader(view: azdata.ModelView): azdata.FlexContainer {
|
||||
const header = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column',
|
||||
width: maxWidth,
|
||||
}).component();
|
||||
const titleComponent = view.modelBuilder.text().withProps({
|
||||
value: loc.DASHBOARD_TITLE,
|
||||
width: '750px',
|
||||
CSSStyles: {
|
||||
...styles.DASHBOARD_TITLE_CSS
|
||||
}
|
||||
}).component();
|
||||
|
||||
const descriptionComponent = view.modelBuilder.text().withProps({
|
||||
value: loc.DASHBOARD_DESCRIPTION,
|
||||
CSSStyles: {
|
||||
...styles.NOTE_CSS
|
||||
}
|
||||
}).component();
|
||||
header.addItems([titleComponent, descriptionComponent], {
|
||||
CSSStyles: {
|
||||
'width': `${maxWidth}px`,
|
||||
'padding-left': '24px'
|
||||
}
|
||||
});
|
||||
return header;
|
||||
public async refresh(): Promise<void> {
|
||||
void this._migrationsTab.refresh();
|
||||
await this._dashboardTab.refresh();
|
||||
}
|
||||
|
||||
private async createTasks(view: azdata.ModelView): Promise<azdata.Component> {
|
||||
const tasksContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'row',
|
||||
width: '100%',
|
||||
}).component();
|
||||
|
||||
const migrateButtonMetadata: IActionMetadata = {
|
||||
title: loc.DASHBOARD_MIGRATE_TASK_BUTTON_TITLE,
|
||||
description: loc.DASHBOARD_MIGRATE_TASK_BUTTON_DESCRIPTION,
|
||||
iconPath: IconPathHelper.sqlMigrationLogo,
|
||||
command: 'sqlmigration.start'
|
||||
};
|
||||
|
||||
const preRequisiteListTitle = view.modelBuilder.text().withProps({
|
||||
value: loc.PRE_REQ_TITLE,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const migrateButton = this.createTaskButton(view, migrateButtonMetadata);
|
||||
|
||||
const preRequisiteListElement = view.modelBuilder.text().withProps({
|
||||
value: [
|
||||
loc.PRE_REQ_1,
|
||||
loc.PRE_REQ_2,
|
||||
loc.PRE_REQ_3
|
||||
],
|
||||
CSSStyles: {
|
||||
...styles.SMALL_NOTE_CSS,
|
||||
'padding-left': '12px',
|
||||
'margin': '-0.5em 0px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const preRequisiteLearnMoreLink = view.modelBuilder.hyperlink().withProps({
|
||||
label: loc.LEARN_MORE,
|
||||
ariaLabel: loc.LEARN_MORE_ABOUT_PRE_REQS,
|
||||
url: 'https://aka.ms/azuresqlmigrationextension',
|
||||
}).component();
|
||||
|
||||
const preReqContainer = view.modelBuilder.flexContainer().withItems([
|
||||
preRequisiteListTitle,
|
||||
preRequisiteListElement,
|
||||
preRequisiteLearnMoreLink
|
||||
]).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
tasksContainer.addItem(migrateButton, {});
|
||||
tasksContainer.addItems([preReqContainer], {
|
||||
CSSStyles: {
|
||||
'margin-left': '20px'
|
||||
}
|
||||
});
|
||||
return tasksContainer;
|
||||
public async onDialogClosed(): Promise<void> {
|
||||
await this._dashboardTab.onDialogClosed();
|
||||
await this._migrationsTab.onDialogClosed();
|
||||
}
|
||||
|
||||
private createTaskButton(view: azdata.ModelView, taskMetaData: IActionMetadata): azdata.Component {
|
||||
const maxHeight: number = 84;
|
||||
const maxWidth: number = 236;
|
||||
const buttonContainer = view.modelBuilder.button().withProps({
|
||||
buttonType: azdata.ButtonType.Informational,
|
||||
description: taskMetaData.description,
|
||||
height: maxHeight,
|
||||
iconHeight: 32,
|
||||
iconPath: taskMetaData.iconPath,
|
||||
iconWidth: 32,
|
||||
label: taskMetaData.title,
|
||||
title: taskMetaData.title,
|
||||
width: maxWidth,
|
||||
CSSStyles: {
|
||||
'border': '1px solid',
|
||||
'display': 'flex',
|
||||
'flex-direction': 'column',
|
||||
'justify-content': 'flex-start',
|
||||
'border-radius': '4px',
|
||||
'transition': 'all .5s ease',
|
||||
}
|
||||
}).component();
|
||||
this._disposables.push(
|
||||
buttonContainer.onDidClick(async () => {
|
||||
if (taskMetaData.command) {
|
||||
await vscode.commands.executeCommand(taskMetaData.command);
|
||||
}
|
||||
}));
|
||||
return view.modelBuilder.divContainer().withItems([buttonContainer]).component();
|
||||
}
|
||||
private _errorDialogIsOpen: boolean = false;
|
||||
|
||||
public async refreshMigrations(): Promise<void> {
|
||||
if (this.isRefreshing) {
|
||||
protected async openErrorDialog(): Promise<void> {
|
||||
if (this._errorDialogIsOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this._migrationStatusCardLoadingContainer.loading = true;
|
||||
let migrations: DatabaseMigration[] = [];
|
||||
try {
|
||||
migrations = await getCurrentMigrations();
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', e);
|
||||
void vscode.window.showErrorMessage(loc.DASHBOARD_REFRESH_MIGRATIONS(e.message));
|
||||
}
|
||||
const tab = azdata.window.createTab(this.errorTitle);
|
||||
tab.registerContent(async (view) => {
|
||||
const flex = view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
view.modelBuilder.text()
|
||||
.withProps({ value: this.errorLabel, CSSStyles: { 'margin': '0px 0px 5px 5px' } })
|
||||
.component(),
|
||||
view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
value: this.errorDescription,
|
||||
readOnly: true,
|
||||
multiline: true,
|
||||
inputType: 'text',
|
||||
rows: 20,
|
||||
CSSStyles: { 'overflow': 'hidden auto', 'margin': '0px 0px 0px 5px' },
|
||||
})
|
||||
.component()
|
||||
])
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: 420,
|
||||
})
|
||||
.withProps({ CSSStyles: { 'margin': '0 10px 0 10px' } })
|
||||
.component();
|
||||
|
||||
const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
|
||||
let warningCount = 0;
|
||||
for (let i = 0; i < inProgressMigrations.length; i++) {
|
||||
if (inProgressMigrations[i].properties.migrationFailureError?.message ||
|
||||
inProgressMigrations[i].properties.migrationStatusDetails?.fileUploadBlockingErrors ||
|
||||
inProgressMigrations[i].properties.migrationStatusDetails?.restoreBlockingReason) {
|
||||
warningCount += 1;
|
||||
}
|
||||
}
|
||||
if (warningCount > 0) {
|
||||
this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount);
|
||||
this._inProgressMigrationButton.container.display = 'none';
|
||||
this._inProgressWarningMigrationButton.container.display = '';
|
||||
} else {
|
||||
this._inProgressMigrationButton.container.display = '';
|
||||
this._inProgressWarningMigrationButton.container.display = 'none';
|
||||
}
|
||||
await view.initializeModel(flex);
|
||||
});
|
||||
|
||||
this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString();
|
||||
this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString();
|
||||
|
||||
this._updateStatusCard(migrations, this._successfulMigrationButton, AdsMigrationStatus.SUCCEEDED, true);
|
||||
this._updateStatusCard(migrations, this._failedMigrationButton, AdsMigrationStatus.FAILED);
|
||||
this._updateStatusCard(migrations, this._completingMigrationButton, AdsMigrationStatus.COMPLETING);
|
||||
this._updateStatusCard(migrations, this._allMigrationButton, AdsMigrationStatus.ALL, true);
|
||||
|
||||
await this._updateSummaryStatus();
|
||||
this.isRefreshing = false;
|
||||
this._migrationStatusCardLoadingContainer.loading = false;
|
||||
}
|
||||
|
||||
private _updateStatusCard(
|
||||
migrations: DatabaseMigration[],
|
||||
card: StatusCard,
|
||||
status: AdsMigrationStatus,
|
||||
show?: boolean): void {
|
||||
const list = filterMigrations(migrations, status);
|
||||
const count = list?.length || 0;
|
||||
card.container.display = count > 0 || show ? '' : 'none';
|
||||
card.count.value = count.toString();
|
||||
}
|
||||
private createStatusCard(
|
||||
cardIconPath: IconPath,
|
||||
cardTitle: string,
|
||||
hasSubtext: boolean = false
|
||||
): StatusCard {
|
||||
const buttonWidth = '400px';
|
||||
const buttonHeight = hasSubtext ? '70px' : '50px';
|
||||
const statusCard = this._view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'width': buttonWidth,
|
||||
'height': buttonHeight,
|
||||
'align-items': 'center',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const statusIcon = this._view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: cardIconPath!.light,
|
||||
iconHeight: 24,
|
||||
iconWidth: 24,
|
||||
height: 32,
|
||||
CSSStyles: { 'margin': '0 8px' }
|
||||
}).component();
|
||||
|
||||
const textContainer = this._view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
|
||||
const cardTitleText = this._view.modelBuilder.text()
|
||||
.withProps({ value: cardTitle })
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
'width': '240px',
|
||||
}
|
||||
}).component();
|
||||
textContainer.addItem(cardTitleText);
|
||||
|
||||
const cardCount = this._view.modelBuilder.text().withProps({
|
||||
value: '0',
|
||||
CSSStyles: {
|
||||
...styles.BIG_NUMBER_CSS,
|
||||
'margin': '0 0 0 8px',
|
||||
'text-align': 'center',
|
||||
}
|
||||
}).component();
|
||||
|
||||
let warningContainer;
|
||||
let warningText;
|
||||
if (hasSubtext) {
|
||||
const warningIcon = this._view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.warning,
|
||||
iconWidth: 12,
|
||||
iconHeight: 12,
|
||||
width: 12,
|
||||
height: 18,
|
||||
}).component();
|
||||
|
||||
const warningDescription = '';
|
||||
warningText = this._view.modelBuilder.text().withProps({ value: warningDescription })
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'padding-left': '8px',
|
||||
const dialog = azdata.window.createModelViewDialog(
|
||||
this.errorTitle,
|
||||
'errorDialog',
|
||||
450,
|
||||
'flyout');
|
||||
dialog.content = [tab];
|
||||
dialog.okButton.label = loc.ERROR_DIALOG_CLEAR_BUTTON_LABEL;
|
||||
dialog.okButton.focused = true;
|
||||
dialog.cancelButton.label = loc.CLOSE;
|
||||
this._disposables.push(
|
||||
dialog.onClosed(async e => {
|
||||
if (e === 'ok') {
|
||||
await this.clearError();
|
||||
}
|
||||
}).component();
|
||||
this._errorDialogIsOpen = false;
|
||||
}));
|
||||
|
||||
warningContainer = this._view.modelBuilder.flexContainer()
|
||||
.withItems(
|
||||
[warningIcon, warningText],
|
||||
{ flex: '0 0 auto' })
|
||||
.withProps({
|
||||
CSSStyles: { 'align-items': 'center' }
|
||||
}).component();
|
||||
|
||||
textContainer.addItem(warningContainer);
|
||||
azdata.window.openDialog(dialog);
|
||||
} catch (error) {
|
||||
this._errorDialogIsOpen = false;
|
||||
}
|
||||
|
||||
statusCard.addItems([
|
||||
statusIcon,
|
||||
textContainer,
|
||||
cardCount,
|
||||
]);
|
||||
|
||||
const compositeButton = this._view.modelBuilder.divContainer()
|
||||
.withItems([statusCard])
|
||||
.withProps({
|
||||
ariaRole: 'button',
|
||||
ariaLabel: loc.SHOW_STATUS,
|
||||
clickable: true,
|
||||
CSSStyles: {
|
||||
'height': buttonHeight,
|
||||
'margin-bottom': '16px',
|
||||
'border': '1px solid',
|
||||
'display': 'flex',
|
||||
'flex-direction': 'column',
|
||||
'justify-content': 'flex-start',
|
||||
'border-radius': '4px',
|
||||
'transition': 'all .5s ease',
|
||||
}
|
||||
}).component();
|
||||
return {
|
||||
container: compositeButton,
|
||||
count: cardCount,
|
||||
textContainer: textContainer,
|
||||
warningContainer: warningContainer,
|
||||
warningText: warningText
|
||||
};
|
||||
}
|
||||
|
||||
private async createFooter(view: azdata.ModelView): Promise<azdata.Component> {
|
||||
const footerContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'row',
|
||||
width: maxWidth,
|
||||
justifyContent: 'flex-start'
|
||||
}).component();
|
||||
const statusContainer = await this.createMigrationStatusContainer(view);
|
||||
const videoLinksContainer = this.createVideoLinks(view);
|
||||
footerContainer.addItem(statusContainer);
|
||||
footerContainer.addItem(videoLinksContainer, {
|
||||
CSSStyles: {
|
||||
'padding-left': '8px',
|
||||
}
|
||||
});
|
||||
|
||||
return footerContainer;
|
||||
}
|
||||
|
||||
private async createMigrationStatusContainer(view: azdata.ModelView): Promise<azdata.FlexContainer> {
|
||||
const statusContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column',
|
||||
width: '400px',
|
||||
height: '385px',
|
||||
justifyContent: 'flex-start',
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'border': '1px solid rgba(0, 0, 0, 0.1)',
|
||||
'padding': '10px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const statusContainerTitle = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.DATABASE_MIGRATION_STATUS,
|
||||
width: '100%',
|
||||
CSSStyles: { ...styles.SECTION_HEADER_CSS }
|
||||
}).component();
|
||||
|
||||
this._refreshButton = view.modelBuilder.button()
|
||||
.withProps({
|
||||
label: loc.REFRESH,
|
||||
iconPath: IconPathHelper.refresh,
|
||||
iconHeight: 16,
|
||||
iconWidth: 16,
|
||||
width: 70,
|
||||
CSSStyles: { 'float': 'right' }
|
||||
}).component();
|
||||
|
||||
const statusHeadingContainer = view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
statusContainerTitle,
|
||||
this._refreshButton,
|
||||
]).withLayout({
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexFlow: 'row',
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
this._refreshButton.onDidClick(async (e) => {
|
||||
this._refreshButton.enabled = false;
|
||||
await this.refreshMigrations();
|
||||
this._refreshButton.enabled = true;
|
||||
}));
|
||||
|
||||
const buttonContainer = view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'justify-content': 'left',
|
||||
'align-iems': 'center',
|
||||
},
|
||||
})
|
||||
.component();
|
||||
|
||||
buttonContainer.addItem(
|
||||
await this.createServiceSelector(this._view));
|
||||
|
||||
this._selectServiceText = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.SELECT_SERVICE_MESSAGE,
|
||||
CSSStyles: {
|
||||
'font-size': '12px',
|
||||
'margin': '10px',
|
||||
'font-weight': '350',
|
||||
'text-align': 'center',
|
||||
'display': 'none'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const header = view.modelBuilder.flexContainer()
|
||||
.withItems([statusHeadingContainer, buttonContainer])
|
||||
.withLayout({ flexFlow: 'column', })
|
||||
.component();
|
||||
|
||||
this._migrationStatusCardsContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
height: '272px',
|
||||
})
|
||||
.withProps({ CSSStyles: { 'overflow': 'hidden auto' } })
|
||||
.component();
|
||||
|
||||
await this._updateSummaryStatus();
|
||||
|
||||
// in progress
|
||||
this._inProgressMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.inProgressMigration,
|
||||
loc.MIGRATION_IN_PROGRESS);
|
||||
this._disposables.push(
|
||||
this._inProgressMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.ONGOING,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._inProgressMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// in progress warning
|
||||
this._inProgressWarningMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.inProgressMigration,
|
||||
loc.MIGRATION_IN_PROGRESS,
|
||||
true);
|
||||
this._disposables.push(
|
||||
this._inProgressWarningMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.ONGOING,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._inProgressWarningMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// successful
|
||||
this._successfulMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.completedMigration,
|
||||
loc.MIGRATION_COMPLETED);
|
||||
this._disposables.push(
|
||||
this._successfulMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.SUCCEEDED,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._successfulMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// completing
|
||||
this._completingMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.completingCutover,
|
||||
loc.MIGRATION_CUTOVER_CARD);
|
||||
this._disposables.push(
|
||||
this._completingMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.COMPLETING,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._completingMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// failed
|
||||
this._failedMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.error,
|
||||
loc.MIGRATION_FAILED);
|
||||
this._disposables.push(
|
||||
this._failedMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.FAILED,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._failedMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// all migrations
|
||||
this._allMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.view,
|
||||
loc.VIEW_ALL);
|
||||
this._disposables.push(
|
||||
this._allMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.ALL,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._allMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
this._migrationStatusCardLoadingContainer = view.modelBuilder.loadingComponent()
|
||||
.withItem(this._migrationStatusCardsContainer)
|
||||
.component();
|
||||
statusContainer.addItem(header, { CSSStyles: { 'margin-bottom': '10px' } });
|
||||
statusContainer.addItem(this._selectServiceText, {});
|
||||
statusContainer.addItem(this._migrationStatusCardLoadingContainer, {});
|
||||
return statusContainer;
|
||||
}
|
||||
|
||||
private async _updateSummaryStatus(): Promise<void> {
|
||||
const serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
|
||||
const isContextValid = isServiceContextValid(serviceContext);
|
||||
await this._selectServiceText.updateCssStyles({ 'display': isContextValid ? 'none' : 'block' });
|
||||
await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': isContextValid ? 'visible' : 'hidden' });
|
||||
this._refreshButton.enabled = isContextValid;
|
||||
}
|
||||
|
||||
private async createServiceSelector(view: azdata.ModelView): Promise<azdata.Component> {
|
||||
const serviceContextLabel = await getSelectedServiceStatus();
|
||||
this._serviceContextButton = view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.sqlMigrationService,
|
||||
iconHeight: 22,
|
||||
iconWidth: 22,
|
||||
label: serviceContextLabel,
|
||||
title: serviceContextLabel,
|
||||
description: loc.MIGRATION_SERVICE_DESCRIPTION,
|
||||
buttonType: azdata.ButtonType.Informational,
|
||||
width: 375,
|
||||
CSSStyles: { ...BUTTON_CSS },
|
||||
})
|
||||
.component();
|
||||
|
||||
this._disposables.push(
|
||||
this._serviceContextButton.onDidClick(async () => {
|
||||
const dialog = new SelectMigrationServiceDialog(this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
|
||||
return this._serviceContextButton;
|
||||
}
|
||||
|
||||
private createVideoLinks(view: azdata.ModelView): azdata.Component {
|
||||
const linksContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: '440px',
|
||||
height: '385px',
|
||||
justifyContent: 'flex-start',
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'border': '1px solid rgba(0, 0, 0, 0.1)',
|
||||
'padding': '10px',
|
||||
'overflow': 'scroll',
|
||||
}
|
||||
}).component();
|
||||
const titleComponent = view.modelBuilder.text().withProps({
|
||||
value: loc.HELP_TITLE,
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS
|
||||
}
|
||||
}).component();
|
||||
|
||||
linksContainer.addItems([titleComponent], {
|
||||
CSSStyles: {
|
||||
'margin-bottom': '16px'
|
||||
}
|
||||
});
|
||||
|
||||
const links = [
|
||||
{
|
||||
title: localize('sql.migration.dashboard.help.link.migrateUsingADS', 'Migrate databases using Azure Data Studio'),
|
||||
description: localize('sql.migration.dashboard.help.description.migrateUsingADS', 'The Azure SQL Migration extension for Azure Data Studio provides capabilities to assess, get right-sized Azure recommendations and migrate SQL Server databases to Azure.'),
|
||||
link: 'https://docs.microsoft.com/azure/dms/migration-using-azure-data-studio'
|
||||
},
|
||||
{
|
||||
title: localize('sql.migration.dashboard.help.link.mi', 'Tutorial: Migrate to Azure SQL Managed Instance (online)'),
|
||||
description: localize('sql.migration.dashboard.help.description.mi', 'A step-by-step tutorial to migrate databases from a SQL Server instance (on-premises or Azure Virtual Machines) to Azure SQL Managed Instance with minimal downtime.'),
|
||||
link: 'https://docs.microsoft.com/azure/dms/tutorial-sql-server-managed-instance-online-ads'
|
||||
},
|
||||
{
|
||||
title: localize('sql.migration.dashboard.help.link.vm', 'Tutorial: Migrate to SQL Server on Azure Virtual Machines (online)'),
|
||||
description: localize('sql.migration.dashboard.help.description.vm', 'A step-by-step tutorial to migrate databases from a SQL Server instance (on-premises) to SQL Server on Azure Virtual Machines with minimal downtime.'),
|
||||
link: 'https://docs.microsoft.com/azure/dms/tutorial-sql-server-to-virtual-machine-online-ads'
|
||||
},
|
||||
{
|
||||
title: localize('sql.migration.dashboard.help.link.dmsGuide', 'Azure Database Migration Guides'),
|
||||
description: localize('sql.migration.dashboard.help.description.dmsGuide', 'A hub of migration articles that provides step-by-step guidance for migrating and modernizing your data assets in Azure.'),
|
||||
link: 'https://docs.microsoft.com/data-migration/'
|
||||
},
|
||||
];
|
||||
|
||||
linksContainer.addItems(links.map(l => this.createLink(view, l)), {});
|
||||
|
||||
const videoLinks: IActionMetadata[] = [];
|
||||
const videosContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'row',
|
||||
width: maxWidth,
|
||||
}).component();
|
||||
videosContainer.addItems(videoLinks.map(l => this.createVideoLink(view, l)), {});
|
||||
linksContainer.addItem(videosContainer);
|
||||
|
||||
return linksContainer;
|
||||
}
|
||||
|
||||
private createLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
|
||||
const maxWidth = 400;
|
||||
const labelsContainer = view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'column',
|
||||
'width': `${maxWidth}px`,
|
||||
'justify-content': 'flex-start',
|
||||
'margin-bottom': '12px'
|
||||
}
|
||||
}).component();
|
||||
const linkContainer = view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'row',
|
||||
'width': `${maxWidth}px`,
|
||||
'justify-content': 'flex-start',
|
||||
'margin-bottom': '4px'
|
||||
}
|
||||
|
||||
}).component();
|
||||
const descriptionComponent = view.modelBuilder.text().withProps({
|
||||
value: linkMetaData.description,
|
||||
width: maxWidth,
|
||||
CSSStyles: {
|
||||
...styles.NOTE_CSS
|
||||
}
|
||||
}).component();
|
||||
const linkComponent = view.modelBuilder.hyperlink().withProps({
|
||||
label: linkMetaData.title!,
|
||||
url: linkMetaData.link!,
|
||||
showLinkIcon: true,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS
|
||||
}
|
||||
}).component();
|
||||
linkContainer.addItem(linkComponent);
|
||||
labelsContainer.addItems([linkContainer, descriptionComponent]);
|
||||
return labelsContainer;
|
||||
}
|
||||
|
||||
private createVideoLink(view: azdata.ModelView, linkMetaData: IActionMetadata): azdata.Component {
|
||||
const maxWidth = 150;
|
||||
const videosContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column',
|
||||
width: maxWidth,
|
||||
justifyContent: 'flex-start'
|
||||
}).component();
|
||||
const video1Container = view.modelBuilder.divContainer().withProps({
|
||||
clickable: true,
|
||||
width: maxWidth,
|
||||
height: '100px'
|
||||
}).component();
|
||||
const descriptionComponent = view.modelBuilder.text().withProps({
|
||||
value: linkMetaData.description,
|
||||
width: maxWidth,
|
||||
height: '50px',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS
|
||||
}
|
||||
}).component();
|
||||
this._disposables.push(
|
||||
video1Container.onDidClick(async () => {
|
||||
if (linkMetaData.link) {
|
||||
await vscode.env.openExternal(vscode.Uri.parse(linkMetaData.link));
|
||||
}
|
||||
}));
|
||||
videosContainer.addItem(video1Container, {
|
||||
CSSStyles: {
|
||||
'background-image': `url(${vscode.Uri.file(<string>linkMetaData.iconPath?.light)})`,
|
||||
'background-repeat': 'no-repeat',
|
||||
'background-position': 'top',
|
||||
'width': `${maxWidth}px`,
|
||||
'height': '104px',
|
||||
'background-size': `${maxWidth}px 120px`
|
||||
}
|
||||
});
|
||||
videosContainer.addItem(descriptionComponent);
|
||||
return videosContainer;
|
||||
private async _updateStatusDisplay(control: azdata.Component, visible: boolean): Promise<void> {
|
||||
await control.updateCssStyles({ 'display': visible ? 'inline' : 'none' });
|
||||
}
|
||||
}
|
||||
|
||||
227
extensions/sql-migration/src/dashboard/tabBase.ts
Normal file
227
extensions/sql-migration/src/dashboard/tabBase.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as loc from '../constants/strings';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { EOL } from 'os';
|
||||
import { DatabaseMigration } from '../api/azure';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
import { getSelectedServiceStatus } from '../models/migrationLocalStorage';
|
||||
|
||||
export const SqlMigrationExtensionId = 'microsoft.sql-migration';
|
||||
export const EmptySettingValue = '-';
|
||||
|
||||
export enum AdsMigrationStatus {
|
||||
ALL = 'all',
|
||||
ONGOING = 'ongoing',
|
||||
SUCCEEDED = 'succeeded',
|
||||
FAILED = 'failed',
|
||||
COMPLETING = 'completing'
|
||||
}
|
||||
|
||||
export const MenuCommands = {
|
||||
Cutover: 'sqlmigration.cutover',
|
||||
ViewDatabase: 'sqlmigration.view.database',
|
||||
ViewTarget: 'sqlmigration.view.target',
|
||||
ViewService: 'sqlmigration.view.service',
|
||||
CopyMigration: 'sqlmigration.copy.migration',
|
||||
CancelMigration: 'sqlmigration.cancel.migration',
|
||||
RetryMigration: 'sqlmigration.retry.migration',
|
||||
StartMigration: 'sqlmigration.start',
|
||||
IssueReporter: 'workbench.action.openIssueReporter',
|
||||
};
|
||||
|
||||
export abstract class TabBase<T> implements azdata.Tab, vscode.Disposable {
|
||||
public content!: azdata.Component;
|
||||
public title: string = '';
|
||||
public id!: string;
|
||||
public icon!: azdata.IconPath | undefined;
|
||||
|
||||
protected context!: vscode.ExtensionContext;
|
||||
protected view!: azdata.ModelView;
|
||||
protected disposables: vscode.Disposable[] = [];
|
||||
protected isRefreshing: boolean = false;
|
||||
protected openMigrationFcn!: (status: AdsMigrationStatus) => Promise<void>;
|
||||
protected statusBar!: DashboardStatusBar;
|
||||
|
||||
protected abstract initialize(view: azdata.ModelView): Promise<void>;
|
||||
|
||||
public abstract refresh(): Promise<void>;
|
||||
|
||||
dispose() {
|
||||
this.disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}
|
||||
|
||||
protected numberCompare(number1: number | undefined, number2: number | undefined, sortDir: number): number {
|
||||
if (!number1) {
|
||||
return sortDir;
|
||||
} else if (!number2) {
|
||||
return -sortDir;
|
||||
}
|
||||
return number1 > number2 ? -sortDir : sortDir;
|
||||
}
|
||||
|
||||
protected stringCompare(string1: string | undefined, string2: string | undefined, sortDir: number): number {
|
||||
if (!string1) {
|
||||
return sortDir;
|
||||
} else if (!string2) {
|
||||
return -sortDir;
|
||||
}
|
||||
return string1.localeCompare(string2) * -sortDir;
|
||||
}
|
||||
|
||||
protected dateCompare(stringDate1: string | undefined, stringDate2: string | undefined, sortDir: number): number {
|
||||
if (!stringDate1) {
|
||||
return sortDir;
|
||||
} else if (!stringDate2) {
|
||||
return -sortDir;
|
||||
}
|
||||
return new Date(stringDate1) > new Date(stringDate2) ? -sortDir : sortDir;
|
||||
}
|
||||
|
||||
protected async updateServiceContext(button: azdata.ButtonComponent): Promise<void> {
|
||||
const label = await getSelectedServiceStatus();
|
||||
if (button.label !== label ||
|
||||
button.title !== label) {
|
||||
|
||||
button.label = label;
|
||||
button.title = label;
|
||||
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
protected createNewMigrationButton(): azdata.ButtonComponent {
|
||||
const newMigrationButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
buttonType: azdata.ButtonType.Normal,
|
||||
label: loc.DESKTOP_MIGRATION_BUTTON_LABEL,
|
||||
description: loc.DESKTOP_MIGRATION_BUTTON_DESCRIPTION,
|
||||
height: 24,
|
||||
iconHeight: 24,
|
||||
iconWidth: 24,
|
||||
iconPath: IconPathHelper.addNew,
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
newMigrationButton.onDidClick(async () => {
|
||||
const actionId = MenuCommands.StartMigration;
|
||||
const args = {
|
||||
extensionId: SqlMigrationExtensionId,
|
||||
issueTitle: loc.DASHBOARD_MIGRATE_TASK_BUTTON_TITLE,
|
||||
};
|
||||
return await vscode.commands.executeCommand(actionId, args);
|
||||
}));
|
||||
return newMigrationButton;
|
||||
}
|
||||
|
||||
protected createNewSupportRequestButton(): azdata.ButtonComponent {
|
||||
const newSupportRequestButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
buttonType: azdata.ButtonType.Normal,
|
||||
label: loc.DESKTOP_SUPPORT_BUTTON_LABEL,
|
||||
description: loc.DESKTOP_SUPPORT_BUTTON_DESCRIPTION,
|
||||
height: 24,
|
||||
iconHeight: 24,
|
||||
iconWidth: 24,
|
||||
iconPath: IconPathHelper.newSupportRequest,
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
newSupportRequestButton.onDidClick(async () => {
|
||||
await vscode.env.openExternal(vscode.Uri.parse(
|
||||
`https://portal.azure.com/#blade/Microsoft_Azure_Support/HelpAndSupportBlade/newsupportrequest`));
|
||||
}));
|
||||
return newSupportRequestButton;
|
||||
}
|
||||
|
||||
protected createFeedbackButton(): azdata.ButtonComponent {
|
||||
const feedbackButton = this.view.modelBuilder.button()
|
||||
.withProps({
|
||||
buttonType: azdata.ButtonType.Normal,
|
||||
label: loc.DESKTOP_FEEDBACK_BUTTON_LABEL,
|
||||
description: loc.DESKTOP_FEEDBACK_BUTTON_DESCRIPTION,
|
||||
height: 24,
|
||||
iconHeight: 24,
|
||||
iconWidth: 24,
|
||||
iconPath: IconPathHelper.sendFeedback,
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
feedbackButton.onDidClick(async () => {
|
||||
const actionId = MenuCommands.IssueReporter;
|
||||
const args = {
|
||||
extensionId: SqlMigrationExtensionId,
|
||||
issueTitle: loc.FEEDBACK_ISSUE_TITLE,
|
||||
};
|
||||
return await vscode.commands.executeCommand(actionId, args);
|
||||
}));
|
||||
return feedbackButton;
|
||||
}
|
||||
|
||||
protected getMigrationErrors(migration: DatabaseMigration): string {
|
||||
const errors = [];
|
||||
errors.push(migration.properties.provisioningError);
|
||||
errors.push(migration.properties.migrationFailureError?.message);
|
||||
errors.push(...migration.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []);
|
||||
errors.push(migration.properties.migrationStatusDetails?.restoreBlockingReason);
|
||||
|
||||
// remove undefined and duplicate error entries
|
||||
return errors
|
||||
.filter((e, i, arr) => e !== undefined && i === arr.indexOf(e))
|
||||
.join(EOL);
|
||||
}
|
||||
|
||||
protected showDialogMessage(
|
||||
title: string,
|
||||
statusMessage: string,
|
||||
errorMessage: string,
|
||||
): void {
|
||||
const tab = azdata.window.createTab(title);
|
||||
tab.registerContent(async (view) => {
|
||||
const flex = view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
view.modelBuilder.text()
|
||||
.withProps({ value: statusMessage })
|
||||
.component(),
|
||||
])
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: 420,
|
||||
})
|
||||
.withProps({ CSSStyles: { 'margin': '0 15px' } })
|
||||
.component();
|
||||
|
||||
if (errorMessage.length > 0) {
|
||||
flex.addItem(
|
||||
view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
value: errorMessage,
|
||||
readOnly: true,
|
||||
multiline: true,
|
||||
inputType: 'text',
|
||||
height: 100,
|
||||
CSSStyles: { 'overflow': 'hidden auto' },
|
||||
})
|
||||
.component()
|
||||
);
|
||||
}
|
||||
|
||||
await view.initializeModel(flex);
|
||||
});
|
||||
|
||||
const dialog = azdata.window.createModelViewDialog(
|
||||
title,
|
||||
'messageDialog',
|
||||
450,
|
||||
'normal');
|
||||
dialog.content = [tab];
|
||||
dialog.okButton.hidden = true;
|
||||
dialog.cancelButton.focused = true;
|
||||
dialog.cancelButton.label = loc.CLOSE;
|
||||
|
||||
azdata.window.openDialog(dialog);
|
||||
}
|
||||
}
|
||||
@@ -864,6 +864,7 @@ export class SqlDatabaseTree {
|
||||
this._descriptionText.value = selectedIssue?.description || '';
|
||||
this._moreInfo.url = selectedIssue?.helpLink || '';
|
||||
this._moreInfo.label = selectedIssue?.displayName || '';
|
||||
this._moreInfo.ariaLabel = selectedIssue?.displayName || '';
|
||||
this._impactedObjects = selectedIssue?.impactedObjects || [];
|
||||
this._recommendationText.value = selectedIssue?.message || constants.NA;
|
||||
|
||||
|
||||
@@ -278,6 +278,7 @@ export class CreateSqlMigrationServiceDialog {
|
||||
|
||||
this._createResourceGroupLink = this._view.modelBuilder.hyperlink().withProps({
|
||||
label: constants.CREATE_NEW,
|
||||
ariaLabel: constants.CREATE_NEW_RESOURCE_GROUP,
|
||||
url: '',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS
|
||||
|
||||
@@ -11,7 +11,8 @@ import { getMigrationTargetInstance, SqlManagedInstance } from '../../api/azure'
|
||||
import { IconPathHelper } from '../../constants/iconPathHelper';
|
||||
import { convertByteSizeToReadableUnit, get12HourTime } from '../../api/utils';
|
||||
import * as styles from '../../constants/styles';
|
||||
import { isBlobMigration } from '../../constants/helper';
|
||||
import { getMigrationTargetTypeEnum, isBlobMigration } from '../../constants/helper';
|
||||
import { MigrationTargetType, ServiceTier } from '../../models/stateMachine';
|
||||
export class ConfirmCutoverDialog {
|
||||
private _dialogObject!: azdata.window.Dialog;
|
||||
private _view!: azdata.ModelView;
|
||||
@@ -32,7 +33,7 @@ export class ConfirmCutoverDialog {
|
||||
}).component();
|
||||
|
||||
const sourceDatabaseText = view.modelBuilder.text().withProps({
|
||||
value: this.migrationCutoverModel._migration.properties.sourceDatabaseName,
|
||||
value: this.migrationCutoverModel.migration.properties.sourceDatabaseName,
|
||||
CSSStyles: {
|
||||
...styles.SMALL_NOTE_CSS,
|
||||
'margin': '4px 0px 8px'
|
||||
@@ -53,7 +54,7 @@ export class ConfirmCutoverDialog {
|
||||
}
|
||||
}).component();
|
||||
|
||||
const fileContainer = isBlobMigration(this.migrationCutoverModel.migrationStatus)
|
||||
const fileContainer = isBlobMigration(this.migrationCutoverModel.migration)
|
||||
? this.createBlobFileContainer()
|
||||
: this.createNetworkShareFileContainer();
|
||||
|
||||
@@ -76,13 +77,13 @@ export class ConfirmCutoverDialog {
|
||||
}).component();
|
||||
|
||||
let infoDisplay = 'none';
|
||||
if (this.migrationCutoverModel._migration.id.toLocaleLowerCase().includes('managedinstances')) {
|
||||
if (getMigrationTargetTypeEnum(this.migrationCutoverModel.migration) === MigrationTargetType.SQLMI) {
|
||||
const targetInstance = await getMigrationTargetInstance(
|
||||
this.migrationCutoverModel._serviceConstext.azureAccount!,
|
||||
this.migrationCutoverModel._serviceConstext.subscription!,
|
||||
this.migrationCutoverModel._migration);
|
||||
this.migrationCutoverModel.serviceConstext.azureAccount!,
|
||||
this.migrationCutoverModel.serviceConstext.subscription!,
|
||||
this.migrationCutoverModel.migration);
|
||||
|
||||
if ((<SqlManagedInstance>targetInstance)?.sku?.tier === 'BusinessCritical') {
|
||||
if ((<SqlManagedInstance>targetInstance)?.sku?.tier === ServiceTier.BusinessCritical) {
|
||||
infoDisplay = 'inline';
|
||||
}
|
||||
}
|
||||
@@ -116,7 +117,7 @@ export class ConfirmCutoverDialog {
|
||||
await this.migrationCutoverModel.startCutover();
|
||||
void vscode.window.showInformationMessage(
|
||||
constants.CUTOVER_IN_PROGRESS(
|
||||
this.migrationCutoverModel._migration.properties.sourceDatabaseName));
|
||||
this.migrationCutoverModel.migration.properties.sourceDatabaseName));
|
||||
}));
|
||||
|
||||
const formBuilder = view.modelBuilder.formContainer().withFormItems(
|
||||
@@ -163,7 +164,7 @@ export class ConfirmCutoverDialog {
|
||||
} catch (e) {
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: e.toString()
|
||||
text: e.message
|
||||
};
|
||||
} finally {
|
||||
refreshLoader.loading = false;
|
||||
@@ -241,7 +242,7 @@ export class ConfirmCutoverDialog {
|
||||
} catch (e) {
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: e.toString()
|
||||
text: e.message
|
||||
};
|
||||
} finally {
|
||||
refreshLoader.loading = false;
|
||||
|
||||
@@ -1,852 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { IconPathHelper } from '../../constants/iconPathHelper';
|
||||
import { BackupFileInfoStatus, MigrationServiceContext, MigrationStatus } from '../../models/migrationLocalStorage';
|
||||
import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
|
||||
import * as loc from '../../constants/strings';
|
||||
import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage, clearDialogMessage, displayDialogErrorMessage } from '../../api/utils';
|
||||
import { EOL } from 'os';
|
||||
import { ConfirmCutoverDialog } from './confirmCutoverDialog';
|
||||
import { logError, TelemetryViews } from '../../telemtery';
|
||||
import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog';
|
||||
import * as styles from '../../constants/styles';
|
||||
import { canRetryMigration, getMigrationStatus, isBlobMigration, isOfflineMigation } from '../../constants/helper';
|
||||
import { DatabaseMigration, getResourceName } from '../../api/azure';
|
||||
|
||||
const statusImageSize: number = 14;
|
||||
|
||||
export class MigrationCutoverDialog {
|
||||
private _dialogObject!: azdata.window.Dialog;
|
||||
private _view!: azdata.ModelView;
|
||||
private _model: MigrationCutoverDialogModel;
|
||||
|
||||
private _databaseTitleName!: azdata.TextComponent;
|
||||
private _cutoverButton!: azdata.ButtonComponent;
|
||||
private _refreshButton!: azdata.ButtonComponent;
|
||||
private _cancelButton!: azdata.ButtonComponent;
|
||||
private _refreshLoader!: azdata.LoadingComponent;
|
||||
private _copyDatabaseMigrationDetails!: azdata.ButtonComponent;
|
||||
private _newSupportRequest!: azdata.ButtonComponent;
|
||||
private _retryButton!: azdata.ButtonComponent;
|
||||
|
||||
private _sourceDatabaseInfoField!: InfoFieldSchema;
|
||||
private _sourceDetailsInfoField!: InfoFieldSchema;
|
||||
private _sourceVersionInfoField!: InfoFieldSchema;
|
||||
private _targetDatabaseInfoField!: InfoFieldSchema;
|
||||
private _targetServerInfoField!: InfoFieldSchema;
|
||||
private _targetVersionInfoField!: InfoFieldSchema;
|
||||
private _migrationStatusInfoField!: InfoFieldSchema;
|
||||
private _fullBackupFileOnInfoField!: InfoFieldSchema;
|
||||
private _backupLocationInfoField!: InfoFieldSchema;
|
||||
private _lastLSNInfoField!: InfoFieldSchema;
|
||||
private _lastAppliedBackupInfoField!: InfoFieldSchema;
|
||||
private _lastAppliedBackupTakenOnInfoField!: InfoFieldSchema;
|
||||
private _currentRestoringFileInfoField!: InfoFieldSchema;
|
||||
|
||||
private _fileCount!: azdata.TextComponent;
|
||||
private _fileTable!: azdata.DeclarativeTableComponent;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
private _emptyTableFill!: azdata.FlexContainer;
|
||||
|
||||
private isRefreshing = false;
|
||||
|
||||
readonly _infoFieldWidth: string = '250px';
|
||||
|
||||
constructor(
|
||||
private readonly _context: vscode.ExtensionContext,
|
||||
private readonly _serviceContext: MigrationServiceContext,
|
||||
private readonly _migration: DatabaseMigration,
|
||||
private readonly _onClosedCallback: () => Promise<void>) {
|
||||
|
||||
this._model = new MigrationCutoverDialogModel(_serviceContext, _migration);
|
||||
this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 'wide');
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
let tab = azdata.window.createTab('');
|
||||
tab.registerContent(async (view: azdata.ModelView) => {
|
||||
try {
|
||||
this._view = view;
|
||||
|
||||
this._fileCount = view.modelBuilder.text().withProps({
|
||||
width: '500px',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS
|
||||
}
|
||||
}).component();
|
||||
|
||||
const rowCssStyle: azdata.CssStyles = {
|
||||
'border': 'none',
|
||||
'text-align': 'left',
|
||||
'border-bottom': '1px solid',
|
||||
'font-size': '12px'
|
||||
};
|
||||
|
||||
const headerCssStyles: azdata.CssStyles = {
|
||||
'border': 'none',
|
||||
'text-align': 'left',
|
||||
'border-bottom': '1px solid',
|
||||
'font-weight': 'bold',
|
||||
'padding-left': '0px',
|
||||
'padding-right': '0px',
|
||||
'font-size': '12px'
|
||||
};
|
||||
|
||||
this._fileTable = view.modelBuilder.declarativeTable().withProps({
|
||||
ariaLabel: loc.ACTIVE_BACKUP_FILES,
|
||||
columns: [
|
||||
{
|
||||
displayName: loc.ACTIVE_BACKUP_FILES,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '230px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyles
|
||||
},
|
||||
{
|
||||
displayName: loc.TYPE,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '90px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyles
|
||||
},
|
||||
{
|
||||
displayName: loc.STATUS,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '60px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyles
|
||||
},
|
||||
{
|
||||
displayName: loc.DATA_UPLOADED,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '120px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyles
|
||||
},
|
||||
{
|
||||
displayName: loc.COPY_THROUGHPUT,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '150px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyles
|
||||
},
|
||||
{
|
||||
displayName: loc.BACKUP_START_TIME,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '130px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyles
|
||||
},
|
||||
{
|
||||
displayName: loc.FIRST_LSN,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '120px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyles
|
||||
},
|
||||
{
|
||||
displayName: loc.LAST_LSN,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '120px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyles
|
||||
}
|
||||
],
|
||||
data: [],
|
||||
width: '1100px',
|
||||
height: '300px',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'display': 'none',
|
||||
'padding-left': '0px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const _emptyTableImage = view.modelBuilder.image().withProps({
|
||||
iconPath: IconPathHelper.emptyTable,
|
||||
iconHeight: '100px',
|
||||
iconWidth: '100px',
|
||||
height: '100px',
|
||||
width: '100px',
|
||||
CSSStyles: {
|
||||
'text-align': 'center'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const _emptyTableText = view.modelBuilder.text().withProps({
|
||||
value: loc.EMPTY_TABLE_TEXT,
|
||||
CSSStyles: {
|
||||
...styles.NOTE_CSS,
|
||||
'margin-top': '8px',
|
||||
'text-align': 'center',
|
||||
'width': '300px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._emptyTableFill = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
alignItems: 'center'
|
||||
}).withItems([
|
||||
_emptyTableImage,
|
||||
_emptyTableText,
|
||||
]).withProps({
|
||||
width: 1000,
|
||||
display: 'none'
|
||||
}).component();
|
||||
|
||||
let formItems = [
|
||||
{ component: this.migrationContainerHeader() },
|
||||
{ component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component() },
|
||||
{ component: await this.migrationInfoGrid() },
|
||||
{ component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component() },
|
||||
{ component: this._fileCount },
|
||||
{ component: this._fileTable },
|
||||
{ component: this._emptyTableFill }
|
||||
];
|
||||
|
||||
const formBuilder = view.modelBuilder.formContainer().withFormItems(
|
||||
formItems,
|
||||
{ horizontal: false }
|
||||
);
|
||||
const form = formBuilder.withLayout({ width: '100%' }).component();
|
||||
|
||||
this._disposables.push(
|
||||
this._view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
|
||||
await view.initializeModel(form);
|
||||
await this.refreshStatus();
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
|
||||
}
|
||||
});
|
||||
this._dialogObject.content = [tab];
|
||||
|
||||
this._dialogObject.cancelButton.hidden = true;
|
||||
this._dialogObject.okButton.label = loc.CLOSE;
|
||||
|
||||
azdata.window.openDialog(this._dialogObject);
|
||||
}
|
||||
|
||||
private migrationContainerHeader(): azdata.FlexContainer {
|
||||
const sqlDatbaseLogo = this._view.modelBuilder.image().withProps({
|
||||
iconPath: IconPathHelper.sqlDatabaseLogo,
|
||||
iconHeight: '32px',
|
||||
iconWidth: '32px',
|
||||
width: '32px',
|
||||
height: '32px'
|
||||
}).component();
|
||||
|
||||
this._databaseTitleName = this._view.modelBuilder.text().withProps({
|
||||
CSSStyles: {
|
||||
...styles.PAGE_TITLE_CSS
|
||||
},
|
||||
width: 950,
|
||||
value: this._model._migration.properties.sourceDatabaseName
|
||||
}).component();
|
||||
|
||||
const databaseSubTitle = this._view.modelBuilder.text().withProps({
|
||||
CSSStyles: {
|
||||
...styles.NOTE_CSS
|
||||
},
|
||||
width: 950,
|
||||
value: loc.DATABASE
|
||||
}).component();
|
||||
|
||||
const titleContainer = this._view.modelBuilder.flexContainer().withItems([
|
||||
this._databaseTitleName,
|
||||
databaseSubTitle
|
||||
]).withLayout({
|
||||
'flexFlow': 'column'
|
||||
}).withProps({
|
||||
width: 950
|
||||
}).component();
|
||||
|
||||
const titleLogoContainer = this._view.modelBuilder.flexContainer().withProps({
|
||||
width: 1000
|
||||
}).component();
|
||||
|
||||
titleLogoContainer.addItem(sqlDatbaseLogo, {
|
||||
flex: '0'
|
||||
});
|
||||
titleLogoContainer.addItem(titleContainer, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'margin-left': '5px',
|
||||
'width': '930px'
|
||||
}
|
||||
});
|
||||
|
||||
const headerActions = this._view.modelBuilder.flexContainer().withLayout({
|
||||
}).withProps({
|
||||
width: 1000
|
||||
}).component();
|
||||
|
||||
this._cutoverButton = this._view.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.cutover,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
label: loc.COMPLETE_CUTOVER,
|
||||
height: '20px',
|
||||
width: '140px',
|
||||
enabled: false,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'display': isOfflineMigation(this._model._migration) ? 'none' : 'block'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._disposables.push(this._cutoverButton.onDidClick(async (e) => {
|
||||
await this.refreshStatus();
|
||||
const dialog = new ConfirmCutoverDialog(this._model);
|
||||
await dialog.initialize();
|
||||
|
||||
if (this._model.CutoverError) {
|
||||
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CUTOVER_ERROR, this._model.CutoverError);
|
||||
}
|
||||
}));
|
||||
|
||||
headerActions.addItem(this._cutoverButton, { flex: '0' });
|
||||
|
||||
this._cancelButton = this._view.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.cancel,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
label: loc.CANCEL_MIGRATION,
|
||||
height: '20px',
|
||||
width: '140px',
|
||||
enabled: false,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._disposables.push(this._cancelButton.onDidClick((e) => {
|
||||
void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, { modal: true }, loc.YES, loc.NO).then(async (v) => {
|
||||
if (v === loc.YES) {
|
||||
await this._model.cancelMigration();
|
||||
await this.refreshStatus();
|
||||
if (this._model.CancelMigrationError) {
|
||||
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CANCELLATION_ERROR, this._model.CancelMigrationError);
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
headerActions.addItem(this._cancelButton, {
|
||||
flex: '0'
|
||||
});
|
||||
|
||||
this._retryButton = this._view.modelBuilder.button().withProps({
|
||||
label: loc.RETRY_MIGRATION,
|
||||
iconPath: IconPathHelper.retry,
|
||||
enabled: false,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
height: '20px',
|
||||
width: '120px',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component();
|
||||
this._disposables.push(this._retryButton.onDidClick(
|
||||
async (e) => {
|
||||
await this.refreshStatus();
|
||||
const retryMigrationDialog = new RetryMigrationDialog(
|
||||
this._context,
|
||||
this._serviceContext,
|
||||
this._migration,
|
||||
this._onClosedCallback);
|
||||
await retryMigrationDialog.openDialog();
|
||||
}
|
||||
));
|
||||
headerActions.addItem(this._retryButton, {
|
||||
flex: '0',
|
||||
});
|
||||
|
||||
this._refreshButton = this._view.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.refresh,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
label: 'Refresh',
|
||||
height: '20px',
|
||||
width: '80px',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
this._refreshButton.onDidClick(async (e) => {
|
||||
this._refreshButton.enabled = false;
|
||||
await this.refreshStatus();
|
||||
this._refreshButton.enabled = true;
|
||||
}));
|
||||
|
||||
headerActions.addItem(this._refreshButton, { flex: '0' });
|
||||
|
||||
this._copyDatabaseMigrationDetails = this._view.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.copy,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
label: loc.COPY_MIGRATION_DETAILS,
|
||||
height: '20px',
|
||||
width: '160px',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._disposables.push(this._copyDatabaseMigrationDetails.onDidClick(async (e) => {
|
||||
await this.refreshStatus();
|
||||
await vscode.env.clipboard.writeText(this.getMigrationDetails());
|
||||
|
||||
void vscode.window.showInformationMessage(loc.DETAILS_COPIED);
|
||||
}));
|
||||
|
||||
headerActions.addItem(this._copyDatabaseMigrationDetails, {
|
||||
flex: '0',
|
||||
CSSStyles: { 'margin-left': '5px' }
|
||||
});
|
||||
|
||||
// create new support request button. Hiding button until sql migration support has been setup.
|
||||
this._newSupportRequest = this._view.modelBuilder.button().withProps({
|
||||
label: loc.NEW_SUPPORT_REQUEST,
|
||||
iconPath: IconPathHelper.newSupportRequest,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
height: '20px',
|
||||
width: '160px',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._disposables.push(this._newSupportRequest.onDidClick(async (e) => {
|
||||
const serviceId = this._model._migration.properties.migrationService;
|
||||
const supportUrl = `https://portal.azure.com/#resource${serviceId}/supportrequest`;
|
||||
await vscode.env.openExternal(vscode.Uri.parse(supportUrl));
|
||||
}));
|
||||
|
||||
headerActions.addItem(this._newSupportRequest, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'margin-left': '5px'
|
||||
}
|
||||
});
|
||||
|
||||
this._refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
|
||||
loading: false,
|
||||
CSSStyles: {
|
||||
'height': '8px',
|
||||
'margin-top': '4px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
headerActions.addItem(this._refreshLoader, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'margin-left': '16px'
|
||||
}
|
||||
});
|
||||
|
||||
const header = this._view.modelBuilder.flexContainer().withItems([
|
||||
titleLogoContainer
|
||||
]).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
width: 1000
|
||||
}
|
||||
}).component();
|
||||
|
||||
header.addItem(headerActions, {
|
||||
'CSSStyles': {
|
||||
'margin-top': '16px'
|
||||
}
|
||||
});
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
private async migrationInfoGrid(): Promise<azdata.FlexContainer> {
|
||||
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
|
||||
container.addItem(infoField.flexContainer, {
|
||||
CSSStyles: {
|
||||
width: this._infoFieldWidth,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const flexServer = this._view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
|
||||
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
|
||||
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
|
||||
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
|
||||
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
|
||||
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
|
||||
|
||||
const flexTarget = this._view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
|
||||
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
|
||||
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
|
||||
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
|
||||
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
|
||||
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
|
||||
|
||||
const _isBlobMigration = isBlobMigration(this._model._migration);
|
||||
const flexStatus = this._view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
|
||||
this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', _isBlobMigration);
|
||||
this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
|
||||
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
|
||||
addInfoFieldToContainer(this._fullBackupFileOnInfoField, flexStatus);
|
||||
addInfoFieldToContainer(this._backupLocationInfoField, flexStatus);
|
||||
|
||||
const flexFile = this._view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
this._lastLSNInfoField = await this.createInfoField(loc.LAST_APPLIED_LSN, '', _isBlobMigration);
|
||||
this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
|
||||
this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', _isBlobMigration);
|
||||
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', !_isBlobMigration);
|
||||
addInfoFieldToContainer(this._lastLSNInfoField, flexFile);
|
||||
addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
|
||||
addInfoFieldToContainer(this._lastAppliedBackupTakenOnInfoField, flexFile);
|
||||
addInfoFieldToContainer(this._currentRestoringFileInfoField, flexFile);
|
||||
|
||||
const flexInfoProps = {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
};
|
||||
|
||||
const flexInfo = this._view.modelBuilder.flexContainer().withProps({
|
||||
width: 1000
|
||||
}).component();
|
||||
flexInfo.addItem(flexServer, flexInfoProps);
|
||||
flexInfo.addItem(flexTarget, flexInfoProps);
|
||||
flexInfo.addItem(flexStatus, flexInfoProps);
|
||||
flexInfo.addItem(flexFile, flexInfoProps);
|
||||
|
||||
return flexInfo;
|
||||
}
|
||||
|
||||
private getMigrationDetails(): string {
|
||||
return JSON.stringify(this._model.migrationStatus, undefined, 2);
|
||||
}
|
||||
|
||||
private async refreshStatus(): Promise<void> {
|
||||
if (this.isRefreshing) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
clearDialogMessage(this._dialogObject);
|
||||
|
||||
await this._cutoverButton.updateCssStyles(
|
||||
{ 'display': isOfflineMigation(this._model._migration) ? 'none' : 'block' });
|
||||
|
||||
this.isRefreshing = true;
|
||||
this._refreshLoader.loading = true;
|
||||
await this._model.fetchStatus();
|
||||
const errors = [];
|
||||
errors.push(this._model.migrationStatus.properties.provisioningError);
|
||||
errors.push(this._model.migrationStatus.properties.migrationFailureError?.message);
|
||||
errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []);
|
||||
errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.restoreBlockingReason);
|
||||
this._dialogObject.message = {
|
||||
// remove undefined and duplicate error entries
|
||||
text: errors
|
||||
.filter((e, i, arr) => e !== undefined && i === arr.indexOf(e))
|
||||
.join(EOL),
|
||||
level: this._model.migrationStatus.properties.migrationStatus === MigrationStatus.InProgress
|
||||
|| this._model.migrationStatus.properties.migrationStatus === MigrationStatus.Completing
|
||||
? azdata.window.MessageLevel.Warning
|
||||
: azdata.window.MessageLevel.Error,
|
||||
description: this.getMigrationDetails()
|
||||
};
|
||||
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
|
||||
const sqlServerName = this._model._migration.properties.sourceServerName;
|
||||
const sourceDatabaseName = this._model._migration.properties.sourceDatabaseName;
|
||||
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
|
||||
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
|
||||
const targetDatabaseName = this._model._migration.name;
|
||||
const targetServerName = getResourceName(this._model._migration.properties.scope);
|
||||
let targetServerVersion;
|
||||
if (this._model.migrationStatus.id.includes('managedInstances')) {
|
||||
targetServerVersion = loc.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
|
||||
} else {
|
||||
targetServerVersion = loc.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
|
||||
}
|
||||
|
||||
let lastAppliedSSN: string;
|
||||
let lastAppliedBackupFileTakenOn: string;
|
||||
|
||||
const tableData: ActiveBackupFileSchema[] = [];
|
||||
|
||||
this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach(
|
||||
(activeBackupSet) => {
|
||||
if (this._shouldDisplayBackupFileTable()) {
|
||||
tableData.push(
|
||||
...activeBackupSet.listOfBackupFiles.map(f => {
|
||||
return {
|
||||
fileName: f.fileName,
|
||||
type: activeBackupSet.backupType,
|
||||
status: f.status,
|
||||
dataUploaded: `${convertByteSizeToReadableUnit(f.dataWritten)} / ${convertByteSizeToReadableUnit(f.totalSize)}`,
|
||||
copyThroughput: (f.copyThroughput) ? (f.copyThroughput / 1024).toFixed(2) : '-',
|
||||
backupStartTime: activeBackupSet.backupStartDate,
|
||||
firstLSN: activeBackupSet.firstLSN,
|
||||
lastLSN: activeBackupSet.lastLSN
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (activeBackupSet.listOfBackupFiles[0].fileName === this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
|
||||
lastAppliedSSN = activeBackupSet.lastLSN;
|
||||
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
|
||||
}
|
||||
});
|
||||
|
||||
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
|
||||
this._sourceDetailsInfoField.text.value = sqlServerName;
|
||||
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
|
||||
|
||||
this._targetDatabaseInfoField.text.value = targetDatabaseName;
|
||||
this._targetServerInfoField.text.value = targetServerName;
|
||||
this._targetVersionInfoField.text.value = targetServerVersion;
|
||||
|
||||
const migrationStatusTextValue = this._getMigrationStatus();
|
||||
this._migrationStatusInfoField.text.value = migrationStatusTextValue ?? '-';
|
||||
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migrationStatusTextValue);
|
||||
|
||||
this._fullBackupFileOnInfoField.text.value = this._model.migrationStatus?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? '-';
|
||||
|
||||
let backupLocation;
|
||||
const _isBlobMigration = isBlobMigration(this._model._migration);
|
||||
// Displaying storage accounts and blob container for azure blob backups.
|
||||
if (_isBlobMigration) {
|
||||
const storageAccountResourceId = this._model._migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId;
|
||||
const blobContainerName = this._model._migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName;
|
||||
backupLocation = storageAccountResourceId && blobContainerName
|
||||
? `${storageAccountResourceId?.split('/').pop()} - ${blobContainerName}`
|
||||
: undefined;
|
||||
} else {
|
||||
const fileShare = this._model._migration.properties.backupConfiguration?.sourceLocation?.fileShare;
|
||||
backupLocation = fileShare?.path! ?? '-';
|
||||
}
|
||||
this._backupLocationInfoField.text.value = backupLocation ?? '-';
|
||||
|
||||
this._lastLSNInfoField.text.value = lastAppliedSSN! ?? '-';
|
||||
this._lastAppliedBackupInfoField.text.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename ?? '-';
|
||||
this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : '-';
|
||||
|
||||
if (_isBlobMigration) {
|
||||
if (!this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) {
|
||||
this._currentRestoringFileInfoField.text.value = '-';
|
||||
} else if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename === this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) {
|
||||
this._currentRestoringFileInfoField.text.value = loc.ALL_BACKUPS_RESTORED;
|
||||
} else {
|
||||
this._currentRestoringFileInfoField.text.value = this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._shouldDisplayBackupFileTable()) {
|
||||
await this._fileCount.updateCssStyles({
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
display: 'inline'
|
||||
});
|
||||
await this._fileTable.updateCssStyles({
|
||||
display: 'inline'
|
||||
});
|
||||
|
||||
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
|
||||
|
||||
if (tableData.length === 0) {
|
||||
await this._emptyTableFill.updateCssStyles({
|
||||
'display': 'flex'
|
||||
});
|
||||
this._fileTable.height = '50px';
|
||||
} else {
|
||||
await this._emptyTableFill.updateCssStyles({
|
||||
'display': 'none'
|
||||
});
|
||||
this._fileTable.height = '300px';
|
||||
|
||||
// Sorting files in descending order of backupStartTime
|
||||
tableData.sort((file1, file2) => new Date(file1.backupStartTime) > new Date(file2.backupStartTime) ? - 1 : 1);
|
||||
|
||||
this._fileTable.data = tableData.map((row) => {
|
||||
return [
|
||||
row.fileName,
|
||||
row.type,
|
||||
row.status,
|
||||
row.dataUploaded,
|
||||
row.copyThroughput,
|
||||
convertIsoTimeToLocalTime(row.backupStartTime).toLocaleString(),
|
||||
row.firstLSN,
|
||||
row.lastLSN
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._cutoverButton.enabled = false;
|
||||
if (migrationStatusTextValue === MigrationStatus.InProgress) {
|
||||
if (_isBlobMigration) {
|
||||
if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
|
||||
this._cutoverButton.enabled = true;
|
||||
}
|
||||
} else {
|
||||
const restoredCount = this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.filter(
|
||||
(a) => a.listOfBackupFiles[0].status === BackupFileInfoStatus.Restored)?.length ?? 0;
|
||||
if (restoredCount > 0) {
|
||||
this._cutoverButton.enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._cancelButton.enabled =
|
||||
migrationStatusTextValue === MigrationStatus.Creating ||
|
||||
migrationStatusTextValue === MigrationStatus.InProgress;
|
||||
|
||||
this._retryButton.enabled = canRetryMigration(migrationStatusTextValue);
|
||||
|
||||
} catch (e) {
|
||||
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_STATUS_REFRESH_ERROR, e);
|
||||
console.log(e);
|
||||
} finally {
|
||||
this.isRefreshing = false;
|
||||
this._refreshLoader.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async createInfoField(label: string, value: string, defaultHidden: boolean = false, iconPath?: azdata.IconPath): Promise<{
|
||||
flexContainer: azdata.FlexContainer,
|
||||
text: azdata.TextComponent,
|
||||
icon?: azdata.ImageComponent
|
||||
}> {
|
||||
const flexContainer = this._view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'column',
|
||||
'padding-right': '12px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
if (defaultHidden) {
|
||||
await flexContainer.updateCssStyles({
|
||||
'display': 'none'
|
||||
});
|
||||
}
|
||||
|
||||
const labelComponent = this._view.modelBuilder.text().withProps({
|
||||
value: label,
|
||||
CSSStyles: {
|
||||
...styles.LIGHT_LABEL_CSS,
|
||||
'margin-bottom': '0',
|
||||
}
|
||||
}).component();
|
||||
flexContainer.addItem(labelComponent);
|
||||
|
||||
const textComponent = this._view.modelBuilder.text().withProps({
|
||||
value: value,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '4px 0 12px',
|
||||
'width': '100%',
|
||||
'overflow': 'visible',
|
||||
'overflow-wrap': 'break-word'
|
||||
}
|
||||
}).component();
|
||||
|
||||
let iconComponent;
|
||||
if (iconPath) {
|
||||
iconComponent = this._view.modelBuilder.image().withProps({
|
||||
iconPath: (iconPath === ' ') ? undefined : iconPath,
|
||||
iconHeight: statusImageSize,
|
||||
iconWidth: statusImageSize,
|
||||
height: statusImageSize,
|
||||
width: statusImageSize,
|
||||
CSSStyles: {
|
||||
'margin': '7px 3px 0 0',
|
||||
'padding': '0'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const iconTextComponent = this._view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
iconComponent,
|
||||
textComponent
|
||||
]).withProps({
|
||||
CSSStyles: {
|
||||
'margin': '0',
|
||||
'padding': '0'
|
||||
},
|
||||
display: 'inline-flex'
|
||||
}).component();
|
||||
flexContainer.addItem(iconTextComponent);
|
||||
} else {
|
||||
flexContainer.addItem(textComponent);
|
||||
}
|
||||
|
||||
return {
|
||||
flexContainer: flexContainer,
|
||||
text: textComponent,
|
||||
icon: iconComponent
|
||||
};
|
||||
}
|
||||
|
||||
private _shouldDisplayBackupFileTable(): boolean {
|
||||
return !isBlobMigration(this._model._migration);
|
||||
}
|
||||
|
||||
private _getMigrationStatus(): string {
|
||||
return this._model.migrationStatus
|
||||
? getMigrationStatus(this._model.migrationStatus)
|
||||
: getMigrationStatus(this._model._migration);
|
||||
}
|
||||
}
|
||||
|
||||
interface ActiveBackupFileSchema {
|
||||
fileName: string,
|
||||
type: string,
|
||||
status: string,
|
||||
dataUploaded: string,
|
||||
copyThroughput: string,
|
||||
backupStartTime: string,
|
||||
firstLSN: string,
|
||||
lastLSN: string
|
||||
}
|
||||
|
||||
interface InfoFieldSchema {
|
||||
flexContainer: azdata.FlexContainer,
|
||||
text: azdata.TextComponent,
|
||||
icon?: azdata.ImageComponent
|
||||
}
|
||||
@@ -7,53 +7,45 @@ import { DatabaseMigration, startMigrationCutover, stopMigration, BackupFileInfo
|
||||
import { BackupFileInfoStatus, MigrationServiceContext } from '../../models/migrationLocalStorage';
|
||||
import { logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../../telemtery';
|
||||
import * as constants from '../../constants/strings';
|
||||
import { EOL } from 'os';
|
||||
import { getMigrationTargetType, getMigrationMode, isBlobMigration } from '../../constants/helper';
|
||||
|
||||
export class MigrationCutoverDialogModel {
|
||||
public CutoverError?: Error;
|
||||
public CancelMigrationError?: Error;
|
||||
public migrationStatus!: DatabaseMigration;
|
||||
|
||||
|
||||
constructor(
|
||||
public _serviceConstext: MigrationServiceContext,
|
||||
public _migration: DatabaseMigration
|
||||
) {
|
||||
}
|
||||
public serviceConstext: MigrationServiceContext,
|
||||
public migration: DatabaseMigration) { }
|
||||
|
||||
public async fetchStatus(): Promise<void> {
|
||||
this.migrationStatus = await getMigrationDetails(
|
||||
this._serviceConstext.azureAccount!,
|
||||
this._serviceConstext.subscription!,
|
||||
this._migration.id,
|
||||
this._migration.properties?.migrationOperationId);
|
||||
const migrationStatus = await getMigrationDetails(
|
||||
this.serviceConstext.azureAccount!,
|
||||
this.serviceConstext.subscription!,
|
||||
this.migration.id,
|
||||
this.migration.properties?.migrationOperationId);
|
||||
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.MigrationCutoverDialog,
|
||||
TelemetryAction.MigrationStatus,
|
||||
{
|
||||
'migrationStatus': this.migrationStatus.properties?.migrationStatus
|
||||
},
|
||||
{}
|
||||
);
|
||||
// Logging status to help debugging.
|
||||
console.log(this.migrationStatus);
|
||||
{ 'migrationStatus': migrationStatus.properties?.migrationStatus },
|
||||
{});
|
||||
|
||||
this.migration = migrationStatus;
|
||||
}
|
||||
|
||||
public async startCutover(): Promise<DatabaseMigration | undefined> {
|
||||
try {
|
||||
this.CutoverError = undefined;
|
||||
if (this._migration) {
|
||||
if (this.migration) {
|
||||
const cutover = await startMigrationCutover(
|
||||
this._serviceConstext.azureAccount!,
|
||||
this._serviceConstext.subscription!,
|
||||
this._migration!);
|
||||
this.serviceConstext.azureAccount!,
|
||||
this.serviceConstext.subscription!,
|
||||
this.migration!);
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.MigrationCutoverDialog,
|
||||
TelemetryAction.CutoverMigration,
|
||||
{
|
||||
...this.getTelemetryProps(this._serviceConstext, this._migration),
|
||||
...this.getTelemetryProps(this.serviceConstext, this.migration),
|
||||
'migrationEndTime': new Date().toString(),
|
||||
},
|
||||
{}
|
||||
@@ -67,30 +59,21 @@ export class MigrationCutoverDialogModel {
|
||||
return undefined!;
|
||||
}
|
||||
|
||||
public async fetchErrors(): Promise<string> {
|
||||
const errors = [];
|
||||
await this.fetchStatus();
|
||||
errors.push(this.migrationStatus.properties.migrationFailureError?.message);
|
||||
return errors
|
||||
.filter((e, i, arr) => e !== undefined && i === arr.indexOf(e))
|
||||
.join(EOL);
|
||||
}
|
||||
|
||||
public async cancelMigration(): Promise<void> {
|
||||
try {
|
||||
this.CancelMigrationError = undefined;
|
||||
if (this.migrationStatus) {
|
||||
if (this.migration) {
|
||||
const cutoverStartTime = new Date().toString();
|
||||
await stopMigration(
|
||||
this._serviceConstext.azureAccount!,
|
||||
this._serviceConstext.subscription!,
|
||||
this.migrationStatus);
|
||||
this.serviceConstext.azureAccount!,
|
||||
this.serviceConstext.subscription!,
|
||||
this.migration);
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.MigrationCutoverDialog,
|
||||
TelemetryAction.CancelMigration,
|
||||
{
|
||||
...this.getTelemetryProps(this._serviceConstext, this._migration),
|
||||
'migrationMode': getMigrationMode(this._migration),
|
||||
...this.getTelemetryProps(this.serviceConstext, this.migration),
|
||||
'migrationMode': getMigrationMode(this.migration),
|
||||
'cutoverStartTime': cutoverStartTime,
|
||||
},
|
||||
{}
|
||||
@@ -104,7 +87,7 @@ export class MigrationCutoverDialogModel {
|
||||
}
|
||||
|
||||
public confirmCutoverStepsString(): string {
|
||||
if (isBlobMigration(this.migrationStatus)) {
|
||||
if (isBlobMigration(this.migration)) {
|
||||
return `${constants.CUTOVER_HELP_STEP1}
|
||||
${constants.CUTOVER_HELP_STEP2_BLOB_CONTAINER}
|
||||
${constants.CUTOVER_HELP_STEP3_BLOB_CONTAINER}`;
|
||||
@@ -116,16 +99,16 @@ export class MigrationCutoverDialogModel {
|
||||
}
|
||||
|
||||
public getLastBackupFileRestoredName(): string | undefined {
|
||||
return this.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename;
|
||||
return this.migration.properties.migrationStatusDetails?.lastRestoredFilename;
|
||||
}
|
||||
|
||||
public getPendingLogBackupsCount(): number | undefined {
|
||||
return this.migrationStatus.properties.migrationStatusDetails?.pendingLogBackupsCount;
|
||||
return this.migration.properties.migrationStatusDetails?.pendingLogBackupsCount;
|
||||
}
|
||||
|
||||
public getPendingFiles(): BackupFileInfo[] {
|
||||
const files: BackupFileInfo[] = [];
|
||||
this.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach(abs => {
|
||||
this.migration.properties.migrationStatusDetails?.activeBackupSets?.forEach(abs => {
|
||||
abs.listOfBackupFiles.forEach(f => {
|
||||
if (f.status !== BackupFileInfoStatus.Restored) {
|
||||
files.push(f);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user