Dev/brih/feature/switch ads to portal context (#18963)

* Add CodeQL Analysis workflow (#10195)

* Add CodeQL Analysis workflow

* Fix path

* dashboard refactor

* update version, readme, minor ui changes

* fix merge issue

* Revert "Add CodeQL Analysis workflow (#10195)"

This reverts commit fe98d586cd75be4758ac544649bb4983accf4acd.

* fix context switching issue

* fix resource id parsing error and mi api version

* mv refresh btn, rm autorefresh, align cards

* remove missed autorefresh code

* improve error handling and messages

* fix typos

* remove duplicate/unnecessary  _populate* calls

* change clear configuration button text

* remove confusing watermark text

* add stale account handling

Co-authored-by: Justin Hutchings <jhutchings1@users.noreply.github.com>
This commit is contained in:
brian-harris
2022-04-12 16:26:40 -07:00
committed by GitHub
parent d98a421035
commit 3a0ac7279a
30 changed files with 2163 additions and 1701 deletions

View File

@@ -1,5 +1,5 @@
# Azure SQL Migration # Azure SQL Migration
The Azure SQL Migration extension in Azure Data Studio brings together a simplified assessment and migration experience that delivers the following capabilities: The Azure SQL Migration extension in Azure Data Studio brings together a simplified assessment, recommendation and migration experience that delivers the following capabilities:
- A responsive user interface that provides an easy-to-navigate step-by-step wizard to deliver an integrated assessment, Azure recommendation and migration experience. - A responsive user interface that provides an easy-to-navigate step-by-step wizard to deliver an integrated assessment, Azure recommendation and migration experience.
- An enhanced assessment engine that can assess SQL Server instances and identify databases that are ready for migration to Azure SQL Managed Instance or SQL Server on Azure Virtual Machines. - An enhanced assessment engine that can assess SQL Server instances and identify databases that are ready for migration to Azure SQL Managed Instance or SQL Server on Azure Virtual Machines.
- SKU recommender to collect performance data from the source SQL Server instance to generate right-sized Azure SQL recommendation. - SKU recommender to collect performance data from the source SQL Server instance to generate right-sized Azure SQL recommendation.

View File

@@ -0,0 +1,32 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_165_1075)">
<path d="M15.0134 7.77127C14.9886 7.02403 14.7001 6.30968 14.199 5.75485C13.6979 5.20003 13.0165 4.84055 12.2756 4.74015C12.2316 3.69638 11.7774 2.71215 11.0118 2.00141C10.2461 1.29067 9.23085 0.910881 8.18669 0.944599C7.34115 0.929662 6.51195 1.17841 5.81424 1.65629C5.11654 2.13417 4.58497 2.81746 4.29336 3.61127C3.40213 3.71901 2.57885 4.14208 1.97238 4.80398C1.36592 5.46588 1.01628 6.32292 0.986694 7.22015C1.02606 8.22935 1.46326 9.18195 2.20276 9.86982C2.94226 10.5577 3.92396 10.9249 4.93336 10.8913H5.28003H11.68C11.7359 10.9005 11.793 10.9005 11.8489 10.8913C12.6784 10.8845 13.4728 10.555 14.0635 9.97262C14.6542 9.39019 14.9949 8.60062 15.0134 7.77127V7.77127Z" fill="url(#paint0_linear_165_1075)"/>
<path d="M7.99998 9.75337C5.95553 9.75337 4.28442 9.22003 4.28442 8.57114V14.8734C4.28442 15.5223 5.92887 16.0467 7.95553 16.0556H7.99998C10.0533 16.0556 11.7155 15.5223 11.7155 14.8734V8.57114C11.7511 9.22003 10.0889 9.75337 7.99998 9.75337Z" fill="url(#paint1_linear_165_1075)"/>
<path d="M11.7512 8.57111C11.7512 9.22 10.089 9.75333 8.03562 9.75333C5.98229 9.75333 4.32007 9.22 4.32007 8.57111C4.32007 7.92222 5.99118 7.38889 8.00007 7.38889C10.009 7.38889 11.7156 7.91333 11.7156 8.57111" fill="#E8E8E8"/>
<path d="M10.8889 8.50005C10.8889 8.91783 9.60894 9.2556 8.03561 9.2556C6.46227 9.2556 5.19116 8.89116 5.19116 8.50005C5.19116 8.10894 6.47116 7.71783 8.00005 7.71783C9.52894 7.71783 10.8534 8.0556 10.8534 8.47338" fill="#50E6FF"/>
<path d="M7.99999 8.65091C7.24037 8.63304 6.48235 8.72891 5.7511 8.93536C6.48136 9.14735 7.23978 9.24627 7.99999 9.22869C8.76315 9.2469 9.52459 9.14798 10.2578 8.93536C9.52361 8.7283 8.76256 8.63242 7.99999 8.65091V8.65091Z" fill="#32BEDD"/>
<path d="M5.58223 4.94452L7.88445 2.63341C7.91006 2.60761 7.94053 2.58714 7.97409 2.57316C8.00765 2.55919 8.04365 2.55199 8.08001 2.55199C8.11636 2.55199 8.15236 2.55919 8.18592 2.57316C8.21948 2.58714 8.24995 2.60761 8.27556 2.63341L10.5778 4.94452C10.5927 4.96225 10.6024 4.98379 10.6057 5.00672C10.6091 5.02965 10.606 5.05305 10.5967 5.0743C10.5875 5.09555 10.5725 5.1138 10.5534 5.12699C10.5344 5.14019 10.512 5.1478 10.4889 5.14897H9.07556C9.04256 5.14897 9.0109 5.16208 8.98757 5.18542C8.96423 5.20876 8.95112 5.24041 8.95112 5.27341V8.13564C8.95416 8.15668 8.94955 8.17812 8.93813 8.19606C8.92672 8.214 8.90925 8.22725 8.88889 8.23341H7.30667C7.29349 8.23481 7.28016 8.23325 7.26766 8.22883C7.25516 8.22441 7.24381 8.21725 7.23443 8.20788C7.22506 8.1985 7.2179 8.18715 7.21348 8.17465C7.20906 8.16215 7.20749 8.14882 7.20889 8.13564V5.26452C7.20956 5.23566 7.20017 5.20746 7.18234 5.18476C7.1645 5.16205 7.13932 5.14626 7.11112 5.14008H5.67112C5.64951 5.13785 5.62887 5.13 5.61123 5.11732C5.5936 5.10464 5.57959 5.08756 5.5706 5.06779C5.56161 5.04802 5.55796 5.02623 5.56 5.00461C5.56204 4.98298 5.5697 4.96227 5.58223 4.94452Z" fill="#F2F2F2"/>
</g>
<defs>
<linearGradient id="paint0_linear_165_1075" x1="8.00003" y1="10.8913" x2="8.00003" y2="0.9446" gradientUnits="userSpaceOnUse">
<stop stop-color="#0078D4"/>
<stop offset="0.16" stop-color="#1380DA"/>
<stop offset="0.53" stop-color="#3C91E5"/>
<stop offset="0.82" stop-color="#559CEC"/>
<stop offset="1" stop-color="#5EA0EF"/>
</linearGradient>
<linearGradient id="paint1_linear_165_1075" x1="4.31998" y1="12.3134" x2="11.7511" y2="12.3134" gradientUnits="userSpaceOnUse">
<stop stop-color="#32BEDD"/>
<stop offset="0.06" stop-color="#37C5E3"/>
<stop offset="0.3" stop-color="#49DDF7"/>
<stop offset="0.45" stop-color="#50E6FF"/>
<stop offset="0.55" stop-color="#50E6FF"/>
<stop offset="0.7" stop-color="#49DDF7"/>
<stop offset="0.94" stop-color="#37C5E3"/>
<stop offset="1" stop-color="#32BEDD"/>
</linearGradient>
<clipPath id="clip0_165_1075">
<rect width="16" height="16" fill="white" transform="translate(0 0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.2187 3.27736C9.48438 3.09246 8.74479 3.00002 8 3.00002C7.2552 3.00002 6.51562 3.09246 5.78125 3.27736C5.04687 3.46226 4.35416 3.73439 3.70312 4.09376C3.05208 4.45314 2.46224 4.89715 1.93359 5.4258C1.40495 5.95444 0.973958 6.56251 0.640625 7.25001C0.432291 7.68231 0.273437 8.12892 0.164062 8.58986C0.0546874 9.05079 0 9.52084 0 10H0.999999C0.999999 9.38543 1.09375 8.80991 1.28125 8.27345C1.46875 7.73699 1.72786 7.24741 2.05859 6.8047C2.38932 6.36199 2.77864 5.96616 3.22656 5.6172C3.67448 5.26825 4.15755 4.97398 4.67578 4.73439C5.19401 4.49481 5.73698 4.31252 6.30469 4.18752C6.87239 4.06252 7.4375 4.00002 8 4.00002C8.5625 4.00002 9.1276 4.06252 9.69531 4.18752C10.263 4.31252 10.806 4.49481 11.3242 4.73439C11.8424 4.97398 12.3255 5.26824 12.7734 5.6172C13.2213 5.96616 13.6107 6.36199 13.9414 6.8047C14.2721 7.24741 14.5312 7.73699 14.7187 8.27345C14.9062 8.80991 15 9.38542 15 10H16C16 9.52085 15.9453 9.0508 15.8359 8.58986C15.7266 8.12892 15.5677 7.6823 15.3594 7.25001C15.026 6.56251 14.595 5.95444 14.0664 5.4258C13.5378 4.89715 12.9479 4.45314 12.2969 4.09376C11.6458 3.73439 10.9531 3.46226 10.2187 3.27736ZM8 7.00001C8.41146 7.00001 8.79948 7.07814 9.16406 7.23439C9.52864 7.39064 9.84766 7.60548 10.1211 7.87892C10.3945 8.15236 10.6094 8.47137 10.7656 8.83595C10.9219 9.20053 11 9.58855 11 10C11 10.4167 10.9219 10.806 10.7656 11.168C10.6094 11.53 10.3945 11.8477 10.1211 12.1211C9.84765 12.3945 9.52864 12.6094 9.16406 12.7656C8.79948 12.9219 8.41146 13 8 13C7.58333 13 7.19401 12.9219 6.83203 12.7656C6.47005 12.6094 6.15234 12.3945 5.87891 12.1211C5.60547 11.8477 5.39062 11.53 5.23437 11.168C5.07812 10.806 5 10.4167 5 10C5 9.58855 5.07812 9.20053 5.23437 8.83595C5.39062 8.47137 5.60547 8.15236 5.87891 7.87892C6.15234 7.60548 6.47005 7.39064 6.83203 7.23439C7.19401 7.07814 7.58333 7.00001 8 7.00001ZM8 12C8.27604 12 8.53516 11.9479 8.77734 11.8438C9.01953 11.7396 9.23177 11.5964 9.41406 11.4141C9.59635 11.2318 9.73958 11.0195 9.84375 10.7774C9.94792 10.5352 10 10.2761 10 10C10 9.72397 9.94792 9.46485 9.84375 9.22267C9.73958 8.98048 9.59635 8.76824 9.41406 8.58595C9.23177 8.40365 9.01953 8.26043 8.77734 8.15626C8.53515 8.05209 8.27604 8.00001 8 8.00001C7.72396 8.00001 7.46484 8.05209 7.22265 8.15626C6.98047 8.26043 6.76823 8.40365 6.58593 8.58595C6.40364 8.76824 6.26041 8.98048 6.15625 9.22267C6.05208 9.46485 6 9.72397 6 10C6 10.2761 6.05208 10.5352 6.15625 10.7774C6.26041 11.0195 6.40364 11.2318 6.58593 11.4141C6.76823 11.5964 6.98047 11.7396 7.22265 11.8438C7.46484 11.9479 7.72396 12 8 12Z" fill="#0078D4"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -2,9 +2,9 @@
"name": "sql-migration", "name": "sql-migration",
"displayName": "%displayName%", "displayName": "%displayName%",
"description": "%description%", "description": "%description%",
"version": "0.1.14", "version": "1.0.0",
"publisher": "Microsoft", "publisher": "Microsoft",
"preview": true, "preview": false,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
"icon": "images/extension.png", "icon": "images/extension.png",
"aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e", "aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e",

View File

@@ -8,6 +8,7 @@
"start-migration-command": "Migrate to Azure SQL", "start-migration-command": "Migrate to Azure SQL",
"send-feedback-command": "Feedback", "send-feedback-command": "Feedback",
"new-support-request-command": "New support request", "new-support-request-command": "New support request",
"refresh-migrations-command": "Refresh",
"migration-context-menu-category": "Migration Context Menu", "migration-context-menu-category": "Migration Context Menu",
"complete-cutover-menu": "Complete cutover", "complete-cutover-menu": "Complete cutover",
"database-details-menu": "Database details", "database-details-menu": "Database details",

View File

@@ -142,10 +142,15 @@ export async function getBlobs(account: azdata.Account, subscription: Subscripti
return blobNames!; return blobNames!;
} }
export async function getSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, sessionId: string): Promise<SqlMigrationService> { export async function getSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string): Promise<SqlMigrationService> {
const sqlMigrationServiceId = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}`;
return await getSqlMigrationServiceById(account, subscription, sqlMigrationServiceId);
}
export async function getSqlMigrationServiceById(account: azdata.Account, subscription: Subscription, sqlMigrationServiceId: string): Promise<SqlMigrationService> {
const api = await getAzureCoreAPI(); const api = await getAzureCoreAPI();
const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}?api-version=2020-09-01-preview`); const path = encodeURI(`${sqlMigrationServiceId}?api-version=2022-01-30-preview`);
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true);
if (response.errors.length > 0) { if (response.errors.length > 0) {
throw new Error(response.errors.toString()); throw new Error(response.errors.toString());
} }
@@ -153,6 +158,20 @@ export async function getSqlMigrationService(account: azdata.Account, subscripti
return response.response.data; return response.response.data;
} }
export async function getSqlMigrationServicesByResourceGroup(account: azdata.Account, subscription: Subscription, resouceGroupName: string): Promise<SqlMigrationService[]> {
const api = await getAzureCoreAPI();
const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resouceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices?api-version=2022-01-30-preview`);
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
sortResourceArrayByName(response.response.data.value);
response.response.data.value.forEach((sms: SqlMigrationService) => {
sms.properties.resourceGroup = getResourceGroupFromId(sms.id);
});
return response.response.data.value;
}
export async function getSqlMigrationServices(account: azdata.Account, subscription: Subscription): Promise<SqlMigrationService[]> { export async function getSqlMigrationServices(account: azdata.Account, subscription: Subscription): Promise<SqlMigrationService[]> {
const api = await getAzureCoreAPI(); const api = await getAzureCoreAPI();
const path = encodeURI(`/subscriptions/${subscription.id}/providers/Microsoft.DataMigration/sqlMigrationServices?api-version=2022-01-30-preview`); const path = encodeURI(`/subscriptions/${subscription.id}/providers/Microsoft.DataMigration/sqlMigrationServices?api-version=2022-01-30-preview`);
@@ -169,7 +188,7 @@ export async function getSqlMigrationServices(account: azdata.Account, subscript
export async function createSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, sessionId: string): Promise<SqlMigrationService> { export async function createSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, sessionId: string): Promise<SqlMigrationService> {
const api = await getAzureCoreAPI(); const api = await getAzureCoreAPI();
const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}?api-version=2020-09-01-preview`); const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}?api-version=2022-01-30-preview`);
const requestBody = { const requestBody = {
'location': regionName 'location': regionName
}; };
@@ -181,7 +200,7 @@ export async function createSqlMigrationService(account: azdata.Account, subscri
const maxRetry = 24; const maxRetry = 24;
let i = 0; let i = 0;
for (i = 0; i < maxRetry; i++) { for (i = 0; i < maxRetry; i++) {
const asyncResponse = await api.makeAzureRestRequest(account, subscription, asyncUrl.replace('https://management.azure.com/', ''), azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); const asyncResponse = await api.makeAzureRestRequest(account, subscription, asyncUrl.replace('https://management.azure.com/', ''), azurecore.HttpRequestMethod.GET, undefined, true);
const creationStatus = asyncResponse.response.data.status; const creationStatus = asyncResponse.response.data.status;
if (creationStatus === ProvisioningState.Succeeded) { if (creationStatus === ProvisioningState.Succeeded) {
break; break;
@@ -196,10 +215,10 @@ export async function createSqlMigrationService(account: azdata.Account, subscri
return response.response.data; return response.response.data;
} }
export async function getSqlMigrationServiceAuthKeys(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, sessionId: string): Promise<SqlMigrationServiceAuthenticationKeys> { export async function getSqlMigrationServiceAuthKeys(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string): Promise<SqlMigrationServiceAuthenticationKeys> {
const api = await getAzureCoreAPI(); const api = await getAzureCoreAPI();
const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}/ListAuthKeys?api-version=2020-09-01-preview`); const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}/ListAuthKeys?api-version=2022-01-30-preview`);
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, getSessionIdHeader(sessionId)); const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true);
if (response.errors.length > 0) { if (response.errors.length > 0) {
throw new Error(response.errors.toString()); throw new Error(response.errors.toString());
} }
@@ -209,15 +228,15 @@ export async function getSqlMigrationServiceAuthKeys(account: azdata.Account, su
}; };
} }
export async function regenerateSqlMigrationServiceAuthKey(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, keyName: string, sessionId: string): Promise<SqlMigrationServiceAuthenticationKeys> { export async function regenerateSqlMigrationServiceAuthKey(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, keyName: string): Promise<SqlMigrationServiceAuthenticationKeys> {
const api = await getAzureCoreAPI(); const api = await getAzureCoreAPI();
const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}/regenerateAuthKeys?api-version=2020-09-01-preview`); const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}/regenerateAuthKeys?api-version=2022-01-30-preview`);
const requestBody = { const requestBody = {
'location': regionName, 'location': regionName,
'keyName': keyName, 'keyName': keyName,
}; };
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, requestBody, true, undefined, getSessionIdHeader(sessionId)); const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, requestBody, true);
if (response.errors.length > 0) { if (response.errors.length > 0) {
throw new Error(response.errors.toString()); throw new Error(response.errors.toString());
} }
@@ -239,10 +258,10 @@ export async function getStorageAccountAccessKeys(account: azdata.Account, subsc
}; };
} }
export async function getSqlMigrationServiceMonitoringData(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationService: string, sessionId: string): Promise<IntegrationRuntimeMonitoringData> { export async function getSqlMigrationServiceMonitoringData(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationService: string): Promise<IntegrationRuntimeMonitoringData> {
const api = await getAzureCoreAPI(); const api = await getAzureCoreAPI();
const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationService}/monitoringData?api-version=2020-09-01-preview`); const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationService}/monitoringData?api-version=2022-01-30-preview`);
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true);
if (response.errors.length > 0) { if (response.errors.length > 0) {
throw new Error(response.errors.toString()); throw new Error(response.errors.toString());
} }
@@ -251,7 +270,7 @@ export async function getSqlMigrationServiceMonitoringData(account: azdata.Accou
export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, regionName: string, targetServer: SqlManagedInstance | SqlVMServer, targetDatabaseName: string, requestBody: StartDatabaseMigrationRequest, sessionId: string): Promise<StartDatabaseMigrationResponse> { export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, regionName: string, targetServer: SqlManagedInstance | SqlVMServer, targetDatabaseName: string, requestBody: StartDatabaseMigrationRequest, sessionId: string): Promise<StartDatabaseMigrationResponse> {
const api = await getAzureCoreAPI(); const api = await getAzureCoreAPI();
const path = encodeURI(`${targetServer.id}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=2020-09-01-preview`); const path = encodeURI(`${targetServer.id}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=2022-01-30-preview`);
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, undefined, getSessionIdHeader(sessionId)); const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, undefined, getSessionIdHeader(sessionId));
if (response.errors.length > 0) { if (response.errors.length > 0) {
throw new Error(response.errors.toString()); throw new Error(response.errors.toString());
@@ -264,70 +283,72 @@ export async function startDatabaseMigration(account: azdata.Account, subscripti
}; };
} }
export async function getMigrationStatus(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration, sessionId: string): Promise<DatabaseMigration> { export async function getMigrationDetails(account: azdata.Account, subscription: Subscription, migrationId: string, migrationOperationId?: string): Promise<DatabaseMigration> {
if (!migration.id) {
throw new Error('NullMigrationId');
}
const migrationOperationId = migration.properties?.migrationOperationId;
if (migrationOperationId === undefined &&
migration.properties.provisioningState === ProvisioningState.Failed) {
return migration;
}
const path = migrationOperationId === undefined const path = migrationOperationId === undefined
? encodeURI(`${migration.id}?$expand=MigrationStatusDetails&api-version=2020-09-01-preview`) ? encodeURI(`${migrationId}?$expand=MigrationStatusDetails&api-version=2022-01-30-preview`)
: encodeURI(`${migration.id}?migrationOperationId=${migrationOperationId}&$expand=MigrationStatusDetails&api-version=2020-09-01-preview`); : encodeURI(`${migrationId}?migrationOperationId=${migrationOperationId}&$expand=MigrationStatusDetails&api-version=2022-01-30-preview`);
const api = await getAzureCoreAPI(); const api = await getAzureCoreAPI();
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, undefined);
if (response.errors.length > 0) { if (response.errors.length > 0) {
throw new Error(response.errors.toString()); throw new Error(response.errors.toString());
} }
const migrationUpdate: DatabaseMigration = response.response.data; return response.response.data;
if (migration.properties) {
migrationUpdate.properties.sourceDatabaseName = migration.properties.sourceDatabaseName;
migrationUpdate.properties.backupConfiguration = migration.properties.backupConfiguration;
migrationUpdate.properties.offlineConfiguration = migration.properties.offlineConfiguration;
}
return migrationUpdate;
} }
export async function getMigrationAsyncOperationDetails(account: azdata.Account, subscription: Subscription, url: string, sessionId: string): Promise<AzureAsyncOperationResource> { export async function getServiceMigrations(account: azdata.Account, subscription: Subscription, resourceId: string): Promise<DatabaseMigration[]> {
const path = encodeURI(`${resourceId}/listMigrations?&api-version=2022-01-30-preview`);
const api = await getAzureCoreAPI(); const api = await getAzureCoreAPI();
const response = await api.makeAzureRestRequest(account, subscription, url.replace('https://management.azure.com/', ''), azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); const response = await api.makeAzureRestRequest(
account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, undefined);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
return response.response.data.value;
}
export async function getMigrationTargetInstance(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise<SqlManagedInstance | SqlVMServer> {
const targetServerId = getMigrationTargetId(migration);
const path = encodeURI(`${targetServerId}?api-version=2021-11-01-preview`);
const api = await getAzureCoreAPI();
const response = await api.makeAzureRestRequest(
account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, undefined);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
return response.response.data;
return <SqlManagedInstance>{};
}
export async function getMigrationAsyncOperationDetails(account: azdata.Account, subscription: Subscription, url: string): Promise<AzureAsyncOperationResource> {
const api = await getAzureCoreAPI();
const response = await api.makeAzureRestRequest(account, subscription, url.replace('https://management.azure.com/', ''), azurecore.HttpRequestMethod.GET, undefined, true);
if (response.errors.length > 0) { if (response.errors.length > 0) {
throw new Error(response.errors.toString()); throw new Error(response.errors.toString());
} }
return response.response.data; return response.response.data;
} }
export async function listMigrationsBySqlMigrationService(account: azdata.Account, subscription: Subscription, sqlMigrationService: SqlMigrationService, sessionId: string): Promise<DatabaseMigration[]> { export async function startMigrationCutover(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise<any> {
const api = await getAzureCoreAPI(); const api = await getAzureCoreAPI();
const path = encodeURI(`${sqlMigrationService.id}/listMigrations?$expand=MigrationStatusDetails&api-version=2020-09-01-preview`); const path = encodeURI(`${migration.id}/operations/${migration.properties.migrationOperationId}/cutover?api-version=2022-01-30-preview`);
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, undefined);
if (response.errors.length > 0) { if (response.errors.length > 0) {
throw new Error(response.errors.toString()); throw new Error(response.errors.toString());
} }
return response.response.data.value; return response.response.data.value;
} }
export async function startMigrationCutover(account: azdata.Account, subscription: Subscription, migrationStatus: DatabaseMigration, sessionId: string): Promise<any> { export async function stopMigration(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise<void> {
const api = await getAzureCoreAPI(); const api = await getAzureCoreAPI();
const path = encodeURI(`${migrationStatus.id}/operations/${migrationStatus.properties.migrationOperationId}/cutover?api-version=2020-09-01-preview`); const path = encodeURI(`${migration.id}/operations/${migration.properties.migrationOperationId}/cancel?api-version=2022-01-30-preview`);
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, getSessionIdHeader(sessionId)); const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, undefined);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
}
return response.response.data.value;
}
export async function stopMigration(account: azdata.Account, subscription: Subscription, migrationStatus: DatabaseMigration, sessionId: string): Promise<void> {
const api = await getAzureCoreAPI();
const path = encodeURI(`${migrationStatus.id}/operations/${migrationStatus.properties.migrationOperationId}/cancel?api-version=2020-09-01-preview`);
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, getSessionIdHeader(sessionId));
if (response.errors.length > 0) { if (response.errors.length > 0) {
throw new Error(response.errors.toString()); throw new Error(response.errors.toString());
} }
@@ -354,6 +375,17 @@ export function sortResourceArrayByName(resourceArray: SortableAzureResources[])
}); });
} }
export function getMigrationTargetId(migration: DatabaseMigration): string {
// `${targetServerId}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=2022-01-30-preview`
const paths = migration.id.split('/providers/Microsoft.DataMigration/', 1);
return paths[0];
}
export function getMigrationTargetName(migration: DatabaseMigration): string {
const targetServerId = getMigrationTargetId(migration);
return getResourceName(targetServerId);
}
export function getResourceGroupFromId(id: string): string { export function getResourceGroupFromId(id: string): string {
return id.replace(RegExp('^(.*?)/resourceGroups/'), '').replace(RegExp('/providers/.*'), '').toLowerCase(); return id.replace(RegExp('^(.*?)/resourceGroups/'), '').replace(RegExp('/providers/.*'), '').toLowerCase();
} }
@@ -452,6 +484,7 @@ export interface DatabaseMigration {
name: string; name: string;
type: string; type: string;
} }
export interface DatabaseMigrationProperties { export interface DatabaseMigrationProperties {
scope: string; scope: string;
provisioningState: 'Succeeded' | 'Failed' | 'Creating'; provisioningState: 'Succeeded' | 'Failed' | 'Creating';
@@ -460,8 +493,8 @@ export interface DatabaseMigrationProperties {
migrationStatusDetails?: MigrationStatusDetails; migrationStatusDetails?: MigrationStatusDetails;
startedOn: string; startedOn: string;
endedOn: string; endedOn: string;
sourceSqlConnection: SqlConnectionInfo;
sourceDatabaseName: string; sourceDatabaseName: string;
sourceServerName: string;
targetDatabaseCollation: string; targetDatabaseCollation: string;
migrationService: string; migrationService: string;
migrationOperationId: string; migrationOperationId: string;
@@ -469,6 +502,7 @@ export interface DatabaseMigrationProperties {
offlineConfiguration: OfflineConfiguration; offlineConfiguration: OfflineConfiguration;
migrationFailureError: ErrorInfo; migrationFailureError: ErrorInfo;
} }
export interface MigrationStatusDetails { export interface MigrationStatusDetails {
migrationState: string; migrationState: string;
startedOn: string; startedOn: string;
@@ -528,6 +562,7 @@ export interface BackupSetInfo {
export interface SourceLocation { export interface SourceLocation {
fileShare?: DatabaseMigrationFileShare; fileShare?: DatabaseMigrationFileShare;
azureBlob?: DatabaseMigrationAzureBlob; azureBlob?: DatabaseMigrationAzureBlob;
fileStorageType: 'FileShare' | 'AzureBlob' | 'None';
} }
export interface TargetLocation { export interface TargetLocation {

View File

@@ -7,8 +7,9 @@ import { window, CategoryValue, DropDownComponent, IconPath } from 'azdata';
import { IconPathHelper } from '../constants/iconPathHelper'; import { IconPathHelper } from '../constants/iconPathHelper';
import { DAYS, HRS, MINUTE, SEC } from '../constants/strings'; import { DAYS, HRS, MINUTE, SEC } from '../constants/strings';
import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel'; import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel';
import { MigrationStatus, MigrationContext, ProvisioningState } from '../models/migrationLocalStorage'; import { MigrationStatus, ProvisioningState } from '../models/migrationLocalStorage';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { DatabaseMigration } from './azure';
export function deepClone<T>(obj: T): T { export function deepClone<T>(obj: T): T {
if (!obj || typeof obj !== 'object') { if (!obj || typeof obj !== 'object') {
@@ -89,40 +90,34 @@ export function convertTimeDifferenceToDuration(startTime: Date, endTime: Date):
} }
} }
export function filterMigrations(databaseMigrations: MigrationContext[], statusFilter: string, databaseNameFilter?: string): MigrationContext[] { export function filterMigrations(databaseMigrations: DatabaseMigration[], statusFilter: string, databaseNameFilter?: string): DatabaseMigration[] {
let filteredMigration: MigrationContext[] = []; let filteredMigration: DatabaseMigration[] = [];
if (statusFilter === AdsMigrationStatus.ALL) { if (statusFilter === AdsMigrationStatus.ALL) {
filteredMigration = databaseMigrations; filteredMigration = databaseMigrations;
} else if (statusFilter === AdsMigrationStatus.ONGOING) { } else if (statusFilter === AdsMigrationStatus.ONGOING) {
filteredMigration = databaseMigrations.filter((value) => { filteredMigration = databaseMigrations.filter(
const status = value.migrationContext.properties?.migrationStatus; value => {
const provisioning = value.migrationContext.properties?.provisioningState; const status = value.properties?.migrationStatus;
return status === MigrationStatus.InProgress return status === MigrationStatus.InProgress
|| status === MigrationStatus.Creating || status === MigrationStatus.Creating
|| provisioning === MigrationStatus.Creating; || value.properties?.provisioningState === MigrationStatus.Creating;
}); });
} else if (statusFilter === AdsMigrationStatus.SUCCEEDED) { } else if (statusFilter === AdsMigrationStatus.SUCCEEDED) {
filteredMigration = databaseMigrations.filter((value) => { filteredMigration = databaseMigrations.filter(
const status = value.migrationContext.properties?.migrationStatus; value => value.properties?.migrationStatus === MigrationStatus.Succeeded);
return status === MigrationStatus.Succeeded;
});
} else if (statusFilter === AdsMigrationStatus.FAILED) { } else if (statusFilter === AdsMigrationStatus.FAILED) {
filteredMigration = databaseMigrations.filter((value) => { filteredMigration = databaseMigrations.filter(
const status = value.migrationContext.properties?.migrationStatus; value =>
const provisioning = value.migrationContext.properties?.provisioningState; value.properties?.migrationStatus === MigrationStatus.Failed ||
return status === MigrationStatus.Failed value.properties?.provisioningState === ProvisioningState.Failed);
|| provisioning === ProvisioningState.Failed;
});
} else if (statusFilter === AdsMigrationStatus.COMPLETING) { } else if (statusFilter === AdsMigrationStatus.COMPLETING) {
filteredMigration = databaseMigrations.filter((value) => { filteredMigration = databaseMigrations.filter(
const status = value.migrationContext.properties?.migrationStatus; value => value.properties?.migrationStatus === MigrationStatus.Completing);
return status === MigrationStatus.Completing;
});
} }
if (databaseNameFilter) { if (databaseNameFilter) {
filteredMigration = filteredMigration.filter((value) => { const filter = databaseNameFilter.toLowerCase();
return value.migrationContext.name.toLowerCase().includes(databaseNameFilter.toLowerCase()); filteredMigration = filteredMigration.filter(
}); migration => migration.name?.toLowerCase().includes(filter));
} }
return filteredMigration; return filteredMigration;
} }
@@ -144,35 +139,36 @@ export function convertIsoTimeToLocalTime(isoTime: string): Date {
return new Date(isoDate.getTime() + (isoDate.getTimezoneOffset() * 60000)); return new Date(isoDate.getTime() + (isoDate.getTimezoneOffset() * 60000));
} }
export type SupportedAutoRefreshIntervals = -1 | 15000 | 30000 | 60000 | 180000 | 300000;
export function selectDefaultDropdownValue(dropDown: DropDownComponent, value?: string, useDisplayName: boolean = true): void { export function selectDefaultDropdownValue(dropDown: DropDownComponent, value?: string, useDisplayName: boolean = true): void {
const selectedIndex = value ? findDropDownItemIndex(dropDown, value, useDisplayName) : -1; if (dropDown.values && dropDown.values.length > 0) {
if (selectedIndex > -1) { const selectedIndex = value ? findDropDownItemIndex(dropDown, value, useDisplayName) : -1;
selectDropDownIndex(dropDown, selectedIndex); if (selectedIndex > -1) {
} else { selectDropDownIndex(dropDown, selectedIndex);
selectDropDownIndex(dropDown, 0); } else {
selectDropDownIndex(dropDown, 0);
}
} }
} }
export function selectDropDownIndex(dropDown: DropDownComponent, index: number): void { export function selectDropDownIndex(dropDown: DropDownComponent, index: number): void {
if (index >= 0 && dropDown.values && index <= dropDown.values.length - 1) { if (dropDown.values && dropDown.values.length > 0) {
const value = dropDown.values[index]; if (index >= 0 && index <= dropDown.values.length - 1) {
dropDown.value = value as CategoryValue; dropDown.value = dropDown.values[index] as CategoryValue;
return;
}
} }
dropDown.value = undefined;
} }
export function findDropDownItemIndex(dropDown: DropDownComponent, value: string, useDisplayName: boolean = true): number { export function findDropDownItemIndex(dropDown: DropDownComponent, value: string, useDisplayName: boolean = true): number {
if (dropDown.values) { if (value && dropDown.values && dropDown.values.length > 0) {
if (useDisplayName) { const searachValue = value?.toLowerCase();
return dropDown.values.findIndex((v: any) => return useDisplayName
(v as CategoryValue)?.displayName?.toLowerCase() === value?.toLowerCase()); ? dropDown.values.findIndex((v: any) =>
} else { (v as CategoryValue)?.displayName?.toLowerCase() === searachValue)
return dropDown.values.findIndex((v: any) => : dropDown.values.findIndex((v: any) =>
(v as CategoryValue)?.name?.toLowerCase() === value?.toLowerCase()); (v as CategoryValue)?.name?.toLowerCase() === searachValue);
}
} }
return -1; return -1;
} }

View File

@@ -4,45 +4,70 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import { MigrationContext, MigrationStatus } from '../models/migrationLocalStorage'; import { DatabaseMigration } from '../api/azure';
import { MigrationMode, MigrationTargetType } from '../models/stateMachine'; import { MigrationStatus } from '../models/migrationLocalStorage';
import { FileStorageType, MigrationMode, MigrationTargetType } from '../models/stateMachine';
import * as loc from './strings'; import * as loc from './strings';
export enum SQLTargetAssetType { export enum SQLTargetAssetType {
SQLMI = 'microsoft.sql/managedinstances', SQLMI = 'microsoft.sql/managedinstances',
SQLVM = 'Microsoft.SqlVirtualMachine/sqlVirtualMachines', SQLVM = 'Microsoft.SqlVirtualMachine/sqlVirtualMachines',
SQLDB = 'Microsoft.Sql/servers',
} }
export function getMigrationTargetType(migration: MigrationContext): string { export function getMigrationTargetType(migration: DatabaseMigration): string {
switch (migration.targetManagedInstance.type) { const id = migration.id?.toLowerCase();
case SQLTargetAssetType.SQLMI: if (id?.indexOf(SQLTargetAssetType.SQLMI.toLowerCase()) > -1) {
return loc.SQL_MANAGED_INSTANCE; return loc.SQL_MANAGED_INSTANCE;
case SQLTargetAssetType.SQLVM:
return loc.SQL_VIRTUAL_MACHINE;
default:
return '';
} }
else if (id?.indexOf(SQLTargetAssetType.SQLVM.toLowerCase()) > -1) {
return loc.SQL_VIRTUAL_MACHINE;
}
else if (id?.indexOf(SQLTargetAssetType.SQLDB.toLowerCase()) > -1) {
return loc.SQL_DATABASE;
}
return '';
} }
export function getMigrationTargetTypeEnum(migration: MigrationContext): MigrationTargetType | undefined { export function getMigrationTargetTypeEnum(migration: DatabaseMigration): MigrationTargetType | undefined {
switch (migration.targetManagedInstance.type) { switch (migration.type) {
case SQLTargetAssetType.SQLMI: case SQLTargetAssetType.SQLMI:
return MigrationTargetType.SQLMI; return MigrationTargetType.SQLMI;
case SQLTargetAssetType.SQLVM: case SQLTargetAssetType.SQLVM:
return MigrationTargetType.SQLVM; return MigrationTargetType.SQLVM;
case SQLTargetAssetType.SQLDB:
return MigrationTargetType.SQLDB;
default: default:
return undefined; return undefined;
} }
} }
export function getMigrationMode(migration: MigrationContext): string { export function getMigrationMode(migration: DatabaseMigration): string {
return migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? loc.OFFLINE : loc.ONLINE; return isOfflineMigation(migration)
? loc.OFFLINE
: loc.ONLINE;
} }
export function getMigrationModeEnum(migration: MigrationContext): MigrationMode { export function getMigrationModeEnum(migration: DatabaseMigration): MigrationMode {
return migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? MigrationMode.OFFLINE : MigrationMode.ONLINE; return isOfflineMigation(migration)
? MigrationMode.OFFLINE
: MigrationMode.ONLINE;
} }
export function isOfflineMigation(migration: DatabaseMigration): boolean {
return migration.properties.offlineConfiguration?.offline === true;
}
export function isBlobMigration(migration: DatabaseMigration): boolean {
return migration?.properties?.backupConfiguration?.sourceLocation?.fileStorageType === FileStorageType.AzureBlob;
}
export function getMigrationStatus(migration: DatabaseMigration): string {
return migration.properties.migrationStatus
?? migration.properties.provisioningState;
}
export function canRetryMigration(status: string | undefined): boolean { export function canRetryMigration(status: string | undefined): boolean {
return status === undefined || return status === undefined ||
status === MigrationStatus.Failed || status === MigrationStatus.Failed ||
@@ -50,15 +75,14 @@ export function canRetryMigration(status: string | undefined): boolean {
status === MigrationStatus.Canceled; status === MigrationStatus.Canceled;
} }
const TABLE_CHECKBOX_INDEX = 0;
const TABLE_DB_NAME_INDEX = 1;
export function selectDatabasesFromList(selectedDbs: string[], databaseTableValues: azdata.DeclarativeTableCellValue[][]): azdata.DeclarativeTableCellValue[][] { export function selectDatabasesFromList(selectedDbs: string[], databaseTableValues: azdata.DeclarativeTableCellValue[][]): azdata.DeclarativeTableCellValue[][] {
const TABLE_CHECKBOX_INDEX = 0;
const TABLE_DB_NAME_INDEX = 1;
const sourceDatabaseNames = selectedDbs?.map(dbName => dbName.toLocaleLowerCase()) || []; const sourceDatabaseNames = selectedDbs?.map(dbName => dbName.toLocaleLowerCase()) || [];
if (sourceDatabaseNames?.length > 0) { if (sourceDatabaseNames?.length > 0) {
for (let i in databaseTableValues) { for (let i in databaseTableValues) {
const row = databaseTableValues[i]; const row = databaseTableValues[i];
const dbName = (row[TABLE_DB_NAME_INDEX].value as string).toLocaleLowerCase(); const dbName = (row[TABLE_DB_NAME_INDEX].value as string)?.toLocaleLowerCase();
if (sourceDatabaseNames.indexOf(dbName) > -1) { if (sourceDatabaseNames.indexOf(dbName) > -1) {
row[TABLE_CHECKBOX_INDEX].value = true; row[TABLE_CHECKBOX_INDEX].value = true;
} }

View File

@@ -43,6 +43,8 @@ export class IconPathHelper {
public static edit: IconPath; public static edit: IconPath;
public static restartDataCollection: IconPath; public static restartDataCollection: IconPath;
public static stop: IconPath; public static stop: IconPath;
public static view: IconPath;
public static sqlMigrationService: IconPath;
public static setExtensionContext(context: vscode.ExtensionContext) { public static setExtensionContext(context: vscode.ExtensionContext) {
IconPathHelper.copy = { IconPathHelper.copy = {
@@ -173,5 +175,13 @@ export class IconPathHelper {
light: context.asAbsolutePath('images/stop.svg'), light: context.asAbsolutePath('images/stop.svg'),
dark: context.asAbsolutePath('images/stop.svg') dark: context.asAbsolutePath('images/stop.svg')
}; };
IconPathHelper.view = {
light: context.asAbsolutePath('images/view.svg'),
dark: context.asAbsolutePath('images/view.svg')
};
IconPathHelper.sqlMigrationService = {
light: context.asAbsolutePath('images/sqlMigrationService.svg'),
dark: context.asAbsolutePath('images/sqlMigrationService.svg'),
};
} }
} }

View File

@@ -65,7 +65,6 @@ export const SKU_RECOMMENDATION_ASSESSMENT_ERROR_DETAIL = localize('sql.migratio
export const REFRESH_ASSESSMENT_BUTTON_LABEL = localize('sql.migration.refresh.assessment.button.label', "Refresh assessment"); export const REFRESH_ASSESSMENT_BUTTON_LABEL = localize('sql.migration.refresh.assessment.button.label', "Refresh assessment");
export const SKU_RECOMMENDATION_CHOOSE_A_TARGET = localize('sql.migration.wizard.sku.choose_a_target', "Choose your Azure SQL target"); export const SKU_RECOMMENDATION_CHOOSE_A_TARGET = localize('sql.migration.wizard.sku.choose_a_target', "Choose your Azure SQL target");
export const SKU_RECOMMENDATION_MI_CARD_TEXT = localize('sql.migration.sku.mi.card.title', "Azure SQL Managed Instance"); export const SKU_RECOMMENDATION_MI_CARD_TEXT = localize('sql.migration.sku.mi.card.title', "Azure SQL Managed Instance");
export const SKU_RECOMMENDATION_DB_CARD_TEXT = localize('sql.migration.sku.db.card.title', "Azure SQL Database"); export const SKU_RECOMMENDATION_DB_CARD_TEXT = localize('sql.migration.sku.db.card.title', "Azure SQL Database");
export const SKU_RECOMMENDATION_VM_CARD_TEXT = localize('sql.migration.sku.vm.card.title', "SQL Server on Azure Virtual Machine"); export const SKU_RECOMMENDATION_VM_CARD_TEXT = localize('sql.migration.sku.vm.card.title', "SQL Server on Azure Virtual Machine");
@@ -299,6 +298,25 @@ export function MI_NOT_READY_ERROR(miName: string, state: string): string {
return localize('sql.migration.mi.not.ready', "The managed instance '{0}' is unavailable for migration because it is currently in the '{1}' state. To continue, select an available managed instance.", miName, state); return localize('sql.migration.mi.not.ready', "The managed instance '{0}' is unavailable for migration because it is currently in the '{1}' state. To continue, select an available managed instance.", miName, state);
} }
export const SELECT_AN_ACCOUNT = localize('sql.migration.select.service.select.a.', "Sign into Azure and select an account");
export const SELECT_A_TENANT = localize('sql.migration.select.service.select.a.tenant', "Select a tenant");
export const SELECT_A_SUBSCRIPTION = localize('sql.migration.select.service.select.a.subscription', "Select a subscription");
export const SELECT_A_LOCATION = localize('sql.migration.select.service.select.a.location', "Select a location");
export const SELECT_A_RESOURCE_GROUP = localize('sql.migration.select.service.select.a.resource.group', "Select a resource group");
export const SELECT_A_SERVICE = localize('sql.migration.select.service.select.a.service', "Select a Database Migration Service");
export const SELECT_ACCOUNT_ERROR = localize('sql.migration.select.service.select.account.error', "An error occurred while loading available Azure accounts.");
export const SELECT_TENANT_ERROR = localize('sql.migration.select.service.select.tenant.error', "An error occurred while loading available Azure account tenants.");
export const SELECT_SUBSCRIPTION_ERROR = localize('sql.migration.select.service.select.subscription.error', "An error occurred while loading account subscriptions. Please check your Azure connection and try again.");
export const SELECT_LOCATION_ERROR = localize('sql.migration.select.service.select.location.error', "An error occurred while loading locations. Please check your Azure connection and try again.");
export const SELECT_RESOURCE_GROUP_ERROR = localize('sql.migration.select.service.select.resource.group.error', "An error occurred while loading available resource groups. Please check your Azure connection and try again.");
export const SELECT_SERVICE_ERROR = localize('sql.migration.select.service.select.service.error', "An error occurred while loading available database migration services. Please check your Azure connection and try again.");
export function ACCOUNT_CREDENTIALS_REFRESH(accountName: string): string {
return localize(
'sql.migration.account.credentials.refresh.required',
"{0} (requires credentials refresh)",
accountName);
}
// database backup page // database backup page
export const DATABASE_BACKUP_PAGE_TITLE = localize('sql.migration.database.page.title', "Database backup"); export const DATABASE_BACKUP_PAGE_TITLE = localize('sql.migration.database.page.title', "Database backup");
export const DATABASE_BACKUP_PAGE_DESCRIPTION = localize('sql.migration.database.page.description', "Select the location of the database backups to use during migration."); export const DATABASE_BACKUP_PAGE_DESCRIPTION = localize('sql.migration.database.page.description', "Select the location of the database backups to use during migration.");
@@ -453,7 +471,7 @@ export const CREATE = localize('sql.migration.create', "Create");
export const CANCEL = localize('sql.migration.cancel', "Cancel"); export const CANCEL = localize('sql.migration.cancel', "Cancel");
export const TYPE = localize('sql.migration.type', "Type"); export const TYPE = localize('sql.migration.type', "Type");
export const USER_ACCOUNT = localize('sql.migration.path.user.account', "User account"); export const USER_ACCOUNT = localize('sql.migration.path.user.account', "User account");
export const VIEW_ALL = localize('sql.migration.view.all', "View all"); export const VIEW_ALL = localize('sql.migration.view.all', "All database migrations");
export const TARGET = localize('sql.migration.target', "Target"); export const TARGET = localize('sql.migration.target', "Target");
export const AZURE_SQL = localize('sql.migration.azure.sql', "Azure SQL"); export const AZURE_SQL = localize('sql.migration.azure.sql', "Azure SQL");
export const CLOSE = localize('sql.migration.close', "Close"); export const CLOSE = localize('sql.migration.close', "Close");
@@ -494,6 +512,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."); export const NOTEBOOK_OPEN_ERROR = localize('sql.migration.notebook.open.error', "Failed to open the migration notebook.");
// Dashboard // 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_TITLE = localize('sql.migration.dashboard.title', "Azure SQL Migration"); 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_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"); export const DASHBOARD_MIGRATE_TASK_BUTTON_TITLE = localize('sql.migration.dashboard.migrate.task.button', "Migrate to Azure SQL");
@@ -505,10 +526,9 @@ export const PRE_REQ_1 = localize('sql.migration.pre.req.1', "Azure account deta
export const PRE_REQ_2 = localize('sql.migration.pre.req.2', "Azure SQL Managed Instance or SQL Server on Azure Virtual Machine"); export const PRE_REQ_2 = localize('sql.migration.pre.req.2', "Azure SQL Managed Instance or SQL Server on Azure Virtual Machine");
export const PRE_REQ_3 = localize('sql.migration.pre.req.3', "Backup location details"); export const PRE_REQ_3 = localize('sql.migration.pre.req.3', "Backup location details");
export const MIGRATION_IN_PROGRESS = localize('sql.migration.migration.in.progress', "Database migrations in progress"); export const MIGRATION_IN_PROGRESS = localize('sql.migration.migration.in.progress', "Database migrations in progress");
export const MIGRATION_FAILED = localize('sql.migration.failed', "Migrations failed"); export const MIGRATION_FAILED = localize('sql.migration.failed', "Database migrations failed");
export const MIGRATION_COMPLETED = localize('sql.migration.migration.completed', "Migrations completed"); export const MIGRATION_COMPLETED = localize('sql.migration.migration.completed', "Database migrations completed");
export const MIGRATION_CUTOVER_CARD = localize('sql.migration.cutover.card', "Completing cutover"); export const MIGRATION_CUTOVER_CARD = localize('sql.migration.cutover.card', "Database migrations completing cutover");
export const MIGRATION_NOT_STARTED = localize('sql.migration.migration.not.started', "Migrations not started");
export const SHOW_STATUS = localize('sql.migration.show.status', "Show status"); export const SHOW_STATUS = localize('sql.migration.show.status', "Show status");
export function MIGRATION_INPROGRESS_WARNING(count: number) { export function MIGRATION_INPROGRESS_WARNING(count: number) {
switch (count) { switch (count) {
@@ -593,6 +613,7 @@ export const NO_PENDING_BACKUPS = localize('sql.migration.no.pending.backups', "
//Migration status dialog //Migration status dialog
export const ADD_ACCOUNT = localize('sql.migration.status.add.account', "Add account"); 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."); export const ADD_ACCOUNT_MESSAGE = localize('sql.migration.status.add.account.MESSAGE', "Add your Azure account to view existing migrations and their status.");
export const SELECT_SERVICE_MESSAGE = localize('sql.migration.status.select.service.MESSAGE', "Select a Database Migration Service to monitor migrations.");
export const STATUS_ALL = localize('sql.migration.status.dropdown.all', "Status: All"); export const STATUS_ALL = localize('sql.migration.status.dropdown.all', "Status: All");
export const STATUS_ONGOING = localize('sql.migration.status.dropdown.ongoing', "Status: Ongoing"); export const STATUS_ONGOING = localize('sql.migration.status.dropdown.ongoing', "Status: Ongoing");
export const STATUS_COMPLETING = localize('sql.migration.status.dropdown.completing', "Status: Completing"); export const STATUS_COMPLETING = localize('sql.migration.status.dropdown.completing', "Status: Completing");
@@ -602,11 +623,13 @@ export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migratio
export const ONLINE = localize('sql.migration.online', "Online"); export const ONLINE = localize('sql.migration.online', "Online");
export const OFFLINE = localize('sql.migration.offline', "Offline"); export const OFFLINE = localize('sql.migration.offline', "Offline");
export const DATABASE = localize('sql.migration.database', "Database"); export const DATABASE = localize('sql.migration.database', "Database");
export const STATUS_COLUMN = localize('sql.migration.database.status.column', "Status");
export const DATABASE_MIGRATION_SERVICE = localize('sql.migration.database.migration.service', "Database Migration Service"); export const DATABASE_MIGRATION_SERVICE = localize('sql.migration.database.migration.service', "Database Migration Service");
export const DURATION = localize('sql.migration.duration', "Duration"); export const DURATION = localize('sql.migration.duration', "Duration");
export const AZURE_SQL_TARGET = localize('sql.migration.azure.sql.target', "Target type"); export const AZURE_SQL_TARGET = localize('sql.migration.azure.sql.target', "Target type");
export const SQL_MANAGED_INSTANCE = localize('sql.migration.sql.managed.instance', "SQL Managed Instance"); export const SQL_MANAGED_INSTANCE = localize('sql.migration.sql.managed.instance', "SQL Managed Instance");
export const SQL_VIRTUAL_MACHINE = localize('sql.migration.sql.virtual.machine', "SQL Virtual Machine"); 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 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 MIGRATION_MODE = localize('sql.migration.cutover.type', "Migration mode");
export const START_TIME = localize('sql.migration.start.time', "Start time"); export const START_TIME = localize('sql.migration.start.time', "Start time");
@@ -745,3 +768,15 @@ export const MIGRATION_RETRY_ERROR = localize('sql.migration.retry.migration.err
export const INVALID_OWNER_URI = localize('sql.migration.invalid.owner.uri.error', 'Cannot connect to the database due to invalid OwnerUri (Parameter \'OwnerUri\')'); export const INVALID_OWNER_URI = localize('sql.migration.invalid.owner.uri.error', 'Cannot connect to the database due to invalid OwnerUri (Parameter \'OwnerUri\')');
export const DATABASE_BACKUP_PAGE_LOAD_ERROR = localize('sql.migration.database.backup.load.error', 'An error occurred while accessing database details.'); export const DATABASE_BACKUP_PAGE_LOAD_ERROR = localize('sql.migration.database.backup.load.error', 'An error occurred while accessing database details.');
// Migration Service Section Dialog
export const MIGRATION_SERVICE_SELECT_TITLE = localize('sql.migration.select.service.title', 'Select Database Migration Service');
export const MIGRATION_SERVICE_SELECT_APPLY_LABEL = localize('sql.migration.select.service.apply.label', 'Apply');
export const MIGRATION_SERVICE_CLEAR = localize('sql.migration.select.service.delete.label', 'Clear');
export const MIGRATION_SERVICE_SELECT_HEADING = localize('sql.migration.select.service.heading', 'Filter the migration list by Database Migration Service');
export const MIGRATION_SERVICE_SELECT_SERVICE_LABEL = localize('sql.migration.select.service.service.label', 'Azure Database Migration Service');
export const MIGRATION_SERVICE_SELECT_SERVICE_PROMPT = localize('sql.migration.select.service.prompt', 'Select a Database Migration Service');
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');

View File

@@ -5,15 +5,17 @@
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { MigrationContext, MigrationLocalStorage } from '../models/migrationLocalStorage';
import { logError, TelemetryViews } from '../telemtery'; import { logError, TelemetryViews } from '../telemtery';
import * as loc from '../constants/strings'; import * as loc from '../constants/strings';
import { IconPath, IconPathHelper } from '../constants/iconPathHelper'; import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
import { MigrationStatusDialog } from '../dialog/migrationStatus/migrationStatusDialog'; import { MigrationStatusDialog } from '../dialog/migrationStatus/migrationStatusDialog';
import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel'; import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel';
import { filterMigrations, SupportedAutoRefreshIntervals } from '../api/utils'; import { filterMigrations } from '../api/utils';
import * as styles from '../constants/styles'; import * as styles from '../constants/styles';
import * as nls from 'vscode-nls'; 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(); const localize = nls.loadMessageBundle();
interface IActionMetadata { interface IActionMetadata {
@@ -25,7 +27,12 @@ interface IActionMetadata {
} }
const maxWidth = 800; const maxWidth = 800;
const refreshFrequency: SupportedAutoRefreshIntervals = 180000; const BUTTON_CSS = {
'font-size': '13px',
'line-height': '18px',
'margin': '4px 0',
'text-align': 'left',
};
interface StatusCard { interface StatusCard {
container: azdata.DivContainer; container: azdata.DivContainer;
@@ -37,39 +44,33 @@ interface StatusCard {
export class DashboardWidget { export class DashboardWidget {
private _context: vscode.ExtensionContext; private _context: vscode.ExtensionContext;
private _migrationStatusCardsContainer!: azdata.FlexContainer; private _migrationStatusCardsContainer!: azdata.FlexContainer;
private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent; private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent;
private _view!: azdata.ModelView; private _view!: azdata.ModelView;
private _inProgressMigrationButton!: StatusCard; private _inProgressMigrationButton!: StatusCard;
private _inProgressWarningMigrationButton!: StatusCard; private _inProgressWarningMigrationButton!: StatusCard;
private _allMigrationButton!: StatusCard;
private _successfulMigrationButton!: StatusCard; private _successfulMigrationButton!: StatusCard;
private _failedMigrationButton!: StatusCard; private _failedMigrationButton!: StatusCard;
private _completingMigrationButton!: StatusCard; private _completingMigrationButton!: StatusCard;
private _notStartedMigrationCard!: StatusCard; private _selectServiceText!: azdata.TextComponent;
private _migrationStatusMap: Map<string, MigrationContext[]> = new Map(); private _serviceContextButton!: azdata.ButtonComponent;
private _viewAllMigrationsButton!: azdata.ButtonComponent; private _refreshButton!: azdata.ButtonComponent;
private _autoRefreshHandle!: NodeJS.Timeout;
private _disposables: vscode.Disposable[] = []; private _disposables: vscode.Disposable[] = [];
private isRefreshing: boolean = false; 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) { constructor(context: vscode.ExtensionContext) {
this._context = context; this._context = context;
} }
private async getCurrentMigrations(): Promise<MigrationContext[]> {
const connectionId = (await azdata.connection.getCurrentConnection()).connectionId;
return this._migrationStatusMap.get(connectionId)!;
}
private async setCurrentMigrations(migrations: MigrationContext[]): Promise<void> {
const connectionId = (await azdata.connection.getCurrentConnection()).connectionId;
this._migrationStatusMap.set(connectionId, migrations);
}
public register(): void { public register(): void {
azdata.ui.registerModelViewProvider('migration.dashboard', async (view) => { azdata.ui.registerModelViewProvider('migration.dashboard', async (view) => {
this._view = view; this._view = view;
@@ -82,7 +83,10 @@ export class DashboardWidget {
const header = this.createHeader(view); const header = this.createHeader(view);
// Files need to have the vscode-file scheme to be loaded by ADS // 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' }); const watermarkUri = vscode.Uri
.file(<string>IconPathHelper.migrationDashboardHeaderBackground.light)
.with({ scheme: 'vscode-file' });
container.addItem(header, { container.addItem(header, {
CSSStyles: { CSSStyles: {
'background-image': ` 'background-image': `
@@ -107,11 +111,11 @@ export class DashboardWidget {
'margin': '0 24px' 'margin': '0 24px'
} }
}); });
this._disposables.push(this._view.onClosed(e => { this._disposables.push(
clearInterval(this._autoRefreshHandle); this._view.onClosed(e => {
this._disposables.forEach( this._disposables.forEach(
d => { try { d.dispose(); } catch { } }); d => { try { d.dispose(); } catch { } });
})); }));
await view.initializeModel(container); await view.initializeModel(container);
await this.refreshMigrations(); await this.refreshMigrations();
@@ -119,8 +123,6 @@ export class DashboardWidget {
} }
private createHeader(view: azdata.ModelView): azdata.FlexContainer { private createHeader(view: azdata.ModelView): azdata.FlexContainer {
this.setAutoRefresh(refreshFrequency);
const header = view.modelBuilder.flexContainer().withLayout({ const header = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column', flexFlow: 'column',
width: maxWidth, width: maxWidth,
@@ -229,95 +231,71 @@ export class DashboardWidget {
'transition': 'all .5s ease', 'transition': 'all .5s ease',
} }
}).component(); }).component();
this._disposables.push(buttonContainer.onDidClick(async () => { this._disposables.push(
if (taskMetaData.command) { buttonContainer.onDidClick(async () => {
await vscode.commands.executeCommand(taskMetaData.command); if (taskMetaData.command) {
} await vscode.commands.executeCommand(taskMetaData.command);
})); }
}));
return view.modelBuilder.divContainer().withItems([buttonContainer]).component(); return view.modelBuilder.divContainer().withItems([buttonContainer]).component();
} }
private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void { public async refreshMigrations(): Promise<void> {
const classVariable = this;
clearInterval(this._autoRefreshHandle);
if (interval !== -1) {
this._autoRefreshHandle = setInterval(async function () { await classVariable.refreshMigrations(); }, interval);
}
}
private async refreshMigrations(): Promise<void> {
if (this.isRefreshing) { if (this.isRefreshing) {
return; return;
} }
this.isRefreshing = true; this.isRefreshing = true;
this._viewAllMigrationsButton.enabled = false;
this._migrationStatusCardLoadingContainer.loading = true; this._migrationStatusCardLoadingContainer.loading = true;
let migrations: DatabaseMigration[] = [];
try { try {
await this.setCurrentMigrations(await this.getMigrations()); migrations = await getCurrentMigrations();
const migrations = await this.getCurrentMigrations(); } catch (e) {
const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING); logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', e);
let warningCount = 0; void vscode.window.showErrorMessage(loc.DASHBOARD_REFRESH_MIGRATIONS(e.message));
for (let i = 0; i < inProgressMigrations.length; i++) {
if (
inProgressMigrations[i].asyncOperationResult?.error?.message ||
inProgressMigrations[i].migrationContext.properties.migrationFailureError?.message ||
inProgressMigrations[i].migrationContext.properties.migrationStatusDetails?.fileUploadBlockingErrors ||
inProgressMigrations[i].migrationContext.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();
const successfulMigration = filterMigrations(migrations, AdsMigrationStatus.SUCCEEDED);
this._successfulMigrationButton.count.value = successfulMigration.length.toString();
const failedMigrations = filterMigrations(migrations, AdsMigrationStatus.FAILED);
const failedCount = failedMigrations.length;
if (failedCount > 0) {
this._failedMigrationButton.container.display = '';
this._failedMigrationButton.count.value = failedCount.toString();
} else {
this._failedMigrationButton.container.display = 'none';
}
const completingCutoverMigrations = filterMigrations(migrations, AdsMigrationStatus.COMPLETING);
const cutoverCount = completingCutoverMigrations.length;
if (cutoverCount > 0) {
this._completingMigrationButton.container.display = '';
this._completingMigrationButton.count.value = cutoverCount.toString();
} else {
this._completingMigrationButton.container.display = 'none';
}
} catch (error) {
logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', error);
} finally {
this.isRefreshing = false;
this._migrationStatusCardLoadingContainer.loading = false;
this._viewAllMigrationsButton.enabled = true;
} }
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;
} }
private async getMigrations(): Promise<MigrationContext[]> { private _updateStatusCard(
const currentConnection = (await azdata.connection.getCurrentConnection()); migrations: DatabaseMigration[],
return await MigrationLocalStorage.getMigrationsBySourceConnections(currentConnection, true); 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( private createStatusCard(
cardIconPath: IconPath, cardIconPath: IconPath,
cardTitle: string, cardTitle: string,
@@ -334,26 +312,27 @@ export class DashboardWidget {
} }
}).component(); }).component();
const statusIcon = this._view.modelBuilder.image().withProps({ const statusIcon = this._view.modelBuilder.image()
iconPath: cardIconPath!.light, .withProps({
iconHeight: 24, iconPath: cardIconPath!.light,
iconWidth: 24, iconHeight: 24,
height: 32, iconWidth: 24,
CSSStyles: { height: 32,
'margin': '0 8px' CSSStyles: { 'margin': '0 8px' }
} }).component();
}).component();
const textContainer = this._view.modelBuilder.flexContainer().withLayout({ const textContainer = this._view.modelBuilder.flexContainer()
flexFlow: 'column' .withLayout({ flexFlow: 'column' })
}).component(); .component();
const cardTitleText = this._view.modelBuilder.text().withProps({ value: cardTitle }).withProps({ const cardTitleText = this._view.modelBuilder.text()
CSSStyles: { .withProps({ value: cardTitle })
...styles.SECTION_HEADER_CSS, .withProps({
'width': '240px' CSSStyles: {
} ...styles.SECTION_HEADER_CSS,
}).component(); 'width': '240px',
}
}).component();
textContainer.addItem(cardTitleText); textContainer.addItem(cardTitleText);
const cardCount = this._view.modelBuilder.text().withProps({ const cardCount = this._view.modelBuilder.text().withProps({
@@ -368,32 +347,31 @@ export class DashboardWidget {
let warningContainer; let warningContainer;
let warningText; let warningText;
if (hasSubtext) { if (hasSubtext) {
const warningIcon = this._view.modelBuilder.image().withProps({ const warningIcon = this._view.modelBuilder.image()
iconPath: IconPathHelper.warning, .withProps({
iconWidth: 12, iconPath: IconPathHelper.warning,
iconHeight: 12, iconWidth: 12,
width: 12, iconHeight: 12,
height: 18 width: 12,
}).component(); height: 18,
}).component();
const warningDescription = ''; const warningDescription = '';
warningText = this._view.modelBuilder.text().withProps({ value: warningDescription }).withProps({ warningText = this._view.modelBuilder.text().withProps({ value: warningDescription })
CSSStyles: { .withProps({
...styles.BODY_CSS, CSSStyles: {
'padding-left': '8px', ...styles.BODY_CSS,
} 'padding-left': '8px',
}).component(); }
}).component();
warningContainer = this._view.modelBuilder.flexContainer().withItems([ warningContainer = this._view.modelBuilder.flexContainer()
warningIcon, .withItems(
warningText [warningIcon, warningText],
], { { flex: '0 0 auto' })
flex: '0 0 auto' .withProps({
}).withProps({ CSSStyles: { 'align-items': 'center' }
CSSStyles: { }).component();
'align-items': 'center'
}
}).component();
textContainer.addItem(warningContainer); textContainer.addItem(warningContainer);
} }
@@ -452,255 +430,243 @@ export class DashboardWidget {
const statusContainer = view.modelBuilder.flexContainer().withLayout({ const statusContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column', flexFlow: 'column',
width: '400px', width: '400px',
height: '360px', height: '385px',
justifyContent: 'flex-start', justifyContent: 'flex-start',
}).withProps({ }).withProps({
CSSStyles: { CSSStyles: {
'border': '1px solid rgba(0, 0, 0, 0.1)', 'border': '1px solid rgba(0, 0, 0, 0.1)',
'padding': '16px' 'padding': '10px',
} }
}).component(); }).component();
const statusContainerTitle = view.modelBuilder.text().withProps({ const statusContainerTitle = view.modelBuilder.text()
value: loc.DATABASE_MIGRATION_STATUS, .withProps({
CSSStyles: { value: loc.DATABASE_MIGRATION_STATUS,
...styles.SECTION_HEADER_CSS width: '100%',
} CSSStyles: { ...styles.SECTION_HEADER_CSS }
}).component(); }).component();
this._viewAllMigrationsButton = view.modelBuilder.hyperlink().withProps({ this._refreshButton = view.modelBuilder.button()
label: loc.VIEW_ALL, .withProps({
url: '', label: loc.REFRESH,
CSSStyles: { iconPath: IconPathHelper.refresh,
...styles.BODY_CSS iconHeight: 16,
} iconWidth: 16,
}).component(); width: 70,
CSSStyles: { 'float': 'right' }
}).component();
this._disposables.push(this._viewAllMigrationsButton.onDidClick(async (e) => { const statusHeadingContainer = view.modelBuilder.flexContainer()
const migrationStatus = await this.getCurrentMigrations(); .withItems([
new MigrationStatusDialog(this._context, migrationStatus ? migrationStatus : await this.getMigrations(), AdsMigrationStatus.ALL).initialize();
}));
const refreshButton = view.modelBuilder.hyperlink().withProps({
label: loc.REFRESH,
url: '',
ariaRole: 'button',
CSSStyles: {
...styles.BODY_CSS,
'text-align': 'right',
}
}).component();
this._disposables.push(refreshButton.onDidClick(async (e) => {
refreshButton.enabled = false;
await this.refreshMigrations();
refreshButton.enabled = true;
}));
const buttonContainer = view.modelBuilder.flexContainer().withLayout({
justifyContent: 'flex-end',
}).component();
buttonContainer.addItem(this._viewAllMigrationsButton, {
CSSStyles: {
'padding-right': '8px',
'border-right': '1px solid',
}
});
buttonContainer.addItem(refreshButton, {
CSSStyles: {
'padding-left': '8px',
}
});
const addAccountImage = view.modelBuilder.image().withProps({
iconPath: IconPathHelper.addAzureAccount,
iconHeight: 100,
iconWidth: 100,
width: 96,
height: 96,
CSSStyles: {
'opacity': '50%',
'margin': '15% auto 10% auto',
'filter': 'drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25))',
'display': 'none'
}
}).component();
const addAccountText = view.modelBuilder.text().withProps({
value: loc.ADD_ACCOUNT_MESSAGE,
width: 198,
height: 34,
CSSStyles: {
...styles.NOTE_CSS,
'margin': 'auto',
'text-align': 'center',
'display': 'none'
}
}).component();
const addAccountButton = view.modelBuilder.button().withProps({
label: loc.ADD_ACCOUNT,
width: '100px',
enabled: true,
CSSStyles: {
'margin': '5% 40%',
'display': 'none'
}
}).component();
this._disposables.push(addAccountButton.onDidClick(async (e) => {
await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount');
addAccountButton.enabled = false;
let accounts = await azdata.accounts.getAllAccounts();
if (accounts.length !== 0) {
await addAccountImage.updateCssStyles({
'display': 'none'
});
await addAccountText.updateCssStyles({
'display': 'none'
});
await addAccountButton.updateCssStyles({
'display': 'none'
});
await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': 'visible' });
await this._viewAllMigrationsButton.updateCssStyles({ 'visibility': 'visible' });
}
await this.refreshMigrations();
}));
const header = view.modelBuilder.flexContainer().withItems(
[
statusContainerTitle, statusContainerTitle,
buttonContainer this._refreshButton,
] ]).withLayout({
).withLayout({ alignContent: 'center',
flexFlow: 'row', alignItems: 'center',
alignItems: 'center' flexFlow: 'row',
}).component(); }).component();
this._migrationStatusCardsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); this._disposables.push(
this._refreshButton.onDidClick(async (e) => {
this._refreshButton.enabled = false;
await this.refreshMigrations();
this._refreshButton.enabled = true;
}));
let accounts = await azdata.accounts.getAllAccounts(); const buttonContainer = view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
'justify-content': 'left',
'align-iems': 'center',
},
})
.component();
if (accounts.length === 0) { buttonContainer.addItem(
await addAccountImage.updateCssStyles({ await this.createServiceSelector(this._view));
'display': 'block'
});
await addAccountText.updateCssStyles({
'display': 'block'
});
await addAccountButton.updateCssStyles({
'display': 'block'
});
await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': 'hidden' });
await this._viewAllMigrationsButton.updateCssStyles({ 'visibility': 'hidden' });
}
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( this._inProgressMigrationButton = this.createStatusCard(
IconPathHelper.inProgressMigration, IconPathHelper.inProgressMigration,
loc.MIGRATION_IN_PROGRESS loc.MIGRATION_IN_PROGRESS);
); this._disposables.push(
this._disposables.push(this._inProgressMigrationButton.container.onDidClick(async (e) => { this._inProgressMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING); const dialog = new MigrationStatusDialog(
dialog.initialize(); this._context,
})); AdsMigrationStatus.ONGOING,
this.onDialogClosed);
await dialog.initialize();
}));
this._migrationStatusCardsContainer.addItem( this._migrationStatusCardsContainer.addItem(
this._inProgressMigrationButton.container this._inProgressMigrationButton.container,
); { flex: '0 0 auto' });
// in progress warning
this._inProgressWarningMigrationButton = this.createStatusCard( this._inProgressWarningMigrationButton = this.createStatusCard(
IconPathHelper.inProgressMigration, IconPathHelper.inProgressMigration,
loc.MIGRATION_IN_PROGRESS, loc.MIGRATION_IN_PROGRESS,
true true);
); this._disposables.push(
this._disposables.push(this._inProgressWarningMigrationButton.container.onDidClick(async (e) => { this._inProgressWarningMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING); const dialog = new MigrationStatusDialog(
dialog.initialize(); this._context,
})); AdsMigrationStatus.ONGOING,
this.onDialogClosed);
await dialog.initialize();
}));
this._migrationStatusCardsContainer.addItem( this._migrationStatusCardsContainer.addItem(
this._inProgressWarningMigrationButton.container this._inProgressWarningMigrationButton.container,
); { flex: '0 0 auto' });
// successful
this._successfulMigrationButton = this.createStatusCard( this._successfulMigrationButton = this.createStatusCard(
IconPathHelper.completedMigration, IconPathHelper.completedMigration,
loc.MIGRATION_COMPLETED loc.MIGRATION_COMPLETED);
); this._disposables.push(
this._disposables.push(this._successfulMigrationButton.container.onDidClick(async (e) => { this._successfulMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.SUCCEEDED); const dialog = new MigrationStatusDialog(
dialog.initialize(); this._context,
})); AdsMigrationStatus.SUCCEEDED,
this.onDialogClosed);
await dialog.initialize();
}));
this._migrationStatusCardsContainer.addItem( this._migrationStatusCardsContainer.addItem(
this._successfulMigrationButton.container this._successfulMigrationButton.container,
); { flex: '0 0 auto' });
// completing
this._completingMigrationButton = this.createStatusCard( this._completingMigrationButton = this.createStatusCard(
IconPathHelper.completingCutover, IconPathHelper.completingCutover,
loc.MIGRATION_CUTOVER_CARD loc.MIGRATION_CUTOVER_CARD);
); this._disposables.push(
this._disposables.push(this._completingMigrationButton.container.onDidClick(async (e) => { this._completingMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.COMPLETING); const dialog = new MigrationStatusDialog(
dialog.initialize(); this._context,
})); AdsMigrationStatus.COMPLETING,
this.onDialogClosed);
await dialog.initialize();
}));
this._migrationStatusCardsContainer.addItem( this._migrationStatusCardsContainer.addItem(
this._completingMigrationButton.container this._completingMigrationButton.container,
); { flex: '0 0 auto' });
// failed
this._failedMigrationButton = this.createStatusCard( this._failedMigrationButton = this.createStatusCard(
IconPathHelper.error, IconPathHelper.error,
loc.MIGRATION_FAILED loc.MIGRATION_FAILED);
); this._disposables.push(
this._disposables.push(this._failedMigrationButton.container.onDidClick(async (e) => { this._failedMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.FAILED); const dialog = new MigrationStatusDialog(
dialog.initialize(); this._context,
})); AdsMigrationStatus.FAILED,
this.onDialogClosed);
await dialog.initialize();
}));
this._migrationStatusCardsContainer.addItem( this._migrationStatusCardsContainer.addItem(
this._failedMigrationButton.container this._failedMigrationButton.container,
); { flex: '0 0 auto' });
this._notStartedMigrationCard = this.createStatusCard( // all migrations
IconPathHelper.notStartedMigration, this._allMigrationButton = this.createStatusCard(
loc.MIGRATION_NOT_STARTED IconPathHelper.view,
); loc.VIEW_ALL);
this._disposables.push(this._notStartedMigrationCard.container.onDidClick((e) => { this._disposables.push(
void vscode.window.showInformationMessage('Feature coming soon'); 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(); this._migrationStatusCardLoadingContainer = view.modelBuilder.loadingComponent()
.withItem(this._migrationStatusCardsContainer)
statusContainer.addItem( .component();
header, { statusContainer.addItem(header, { CSSStyles: { 'margin-bottom': '10px' } });
CSSStyles: { statusContainer.addItem(this._selectServiceText, {});
'margin-bottom': '16px'
}
}
);
statusContainer.addItem(addAccountImage, {});
statusContainer.addItem(addAccountText, {});
statusContainer.addItem(addAccountButton, {});
statusContainer.addItem(this._migrationStatusCardLoadingContainer, {}); statusContainer.addItem(this._migrationStatusCardLoadingContainer, {});
return statusContainer; 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 { private createVideoLinks(view: azdata.ModelView): azdata.Component {
const linksContainer = view.modelBuilder.flexContainer().withLayout({ const linksContainer = view.modelBuilder.flexContainer()
flexFlow: 'column', .withLayout({
width: '400px', flexFlow: 'column',
height: '360px', width: '440px',
justifyContent: 'flex-start', height: '385px',
}).withProps({ justifyContent: 'flex-start',
CSSStyles: { }).withProps({
'border': '1px solid rgba(0, 0, 0, 0.1)', CSSStyles: {
'padding': '16px', 'border': '1px solid rgba(0, 0, 0, 0.1)',
'overflow': 'scroll', 'padding': '10px',
} 'overflow': 'scroll',
}).component(); }
}).component();
const titleComponent = view.modelBuilder.text().withProps({ const titleComponent = view.modelBuilder.text().withProps({
value: loc.HELP_TITLE, value: loc.HELP_TITLE,
CSSStyles: { CSSStyles: {
@@ -809,11 +775,12 @@ export class DashboardWidget {
...styles.BODY_CSS ...styles.BODY_CSS
} }
}).component(); }).component();
this._disposables.push(video1Container.onDidClick(async () => { this._disposables.push(
if (linkMetaData.link) { video1Container.onDidClick(async () => {
await vscode.env.openExternal(vscode.Uri.parse(linkMetaData.link)); if (linkMetaData.link) {
} await vscode.env.openExternal(vscode.Uri.parse(linkMetaData.link));
})); }
}));
videosContainer.addItem(video1Container, { videosContainer.addItem(video1Container, {
CSSStyles: { CSSStyles: {
'background-image': `url(${vscode.Uri.file(<string>linkMetaData.iconPath?.light)})`, 'background-image': `url(${vscode.Uri.file(<string>linkMetaData.iconPath?.light)})`,

View File

@@ -22,7 +22,10 @@ export class SavedAssessmentDialog {
private context: vscode.ExtensionContext; private context: vscode.ExtensionContext;
private _disposables: vscode.Disposable[] = []; private _disposables: vscode.Disposable[] = [];
constructor(context: vscode.ExtensionContext, stateModel: MigrationStateModel) { constructor(
context: vscode.ExtensionContext,
stateModel: MigrationStateModel,
private readonly _onClosedCallback: () => Promise<void>) {
this.stateModel = stateModel; this.stateModel = stateModel;
this.context = context; this.context = context;
} }
@@ -53,7 +56,7 @@ export class SavedAssessmentDialog {
dialog.registerCloseValidator(async () => { dialog.registerCloseValidator(async () => {
if (this.stateModel.resumeAssessment) { if (this.stateModel.resumeAssessment) {
if (!this.stateModel.loadSavedInfo()) { if (await !this.stateModel.loadSavedInfo()) {
void vscode.window.showInformationMessage(constants.OPEN_SAVED_INFO_ERROR); void vscode.window.showInformationMessage(constants.OPEN_SAVED_INFO_ERROR);
return false; return false;
} }
@@ -77,7 +80,11 @@ export class SavedAssessmentDialog {
} }
protected async execute() { protected async execute() {
const wizardController = new WizardController(this.context, this.stateModel); const wizardController = new WizardController(
this.context,
this.stateModel,
this._onClosedCallback);
await wizardController.openWizard(this.stateModel.sourceConnectionId); await wizardController.openWizard(this.stateModel.sourceConnectionId);
this._isOpen = false; this._isOpen = false;
} }
@@ -103,11 +110,11 @@ export class SavedAssessmentDialog {
checked: true checked: true
}).component(); }).component();
radioStart.onDidChangeCheckedState((e) => { this._disposables.push(radioStart.onDidChangeCheckedState((e) => {
if (e) { if (e) {
this.stateModel.resumeAssessment = false; this.stateModel.resumeAssessment = false;
} }
}); }));
const radioContinue = view.modelBuilder.radioButton().withProps({ const radioContinue = view.modelBuilder.radioButton().withProps({
label: constants.RESUME_SESSION, label: constants.RESUME_SESSION,
name: buttonGroup, name: buttonGroup,
@@ -117,11 +124,11 @@ export class SavedAssessmentDialog {
checked: false checked: false
}).component(); }).component();
radioContinue.onDidChangeCheckedState((e) => { this._disposables.push(radioContinue.onDidChangeCheckedState((e) => {
if (e) { if (e) {
this.stateModel.resumeAssessment = true; this.stateModel.resumeAssessment = true;
} }
}); }));
const flex = view.modelBuilder.flexContainer() const flex = view.modelBuilder.flexContainer()
.withLayout({ .withLayout({

View File

@@ -95,7 +95,14 @@ export class CreateSqlMigrationServiceDialog {
try { try {
clearDialogMessage(this._dialogObject); clearDialogMessage(this._dialogObject);
this._selectedResourceGroup = resourceGroup; this._selectedResourceGroup = resourceGroup;
this._createdMigrationService = await createSqlMigrationService(this._model._azureAccount, subscription, resourceGroup, location, serviceName!, this._model._sessionId); this._createdMigrationService = await createSqlMigrationService(
this._model._azureAccount,
subscription,
resourceGroup,
location,
serviceName!,
this._model._sessionId);
if (this._createdMigrationService.error) { if (this._createdMigrationService.error) {
this.setDialogMessage(`${this._createdMigrationService.error.code} : ${this._createdMigrationService.error.message}`); this.setDialogMessage(`${this._createdMigrationService.error.code} : ${this._createdMigrationService.error.message}`);
this._statusLoadingComponent.loading = false; this._statusLoadingComponent.loading = false;
@@ -490,7 +497,12 @@ export class CreateSqlMigrationServiceDialog {
for (let i = 0; i < maxRetries; i++) { for (let i = 0; i < maxRetries; i++) {
try { try {
clearDialogMessage(this._dialogObject); clearDialogMessage(this._dialogObject);
migrationServiceStatus = await getSqlMigrationService(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService.name, this._model._sessionId); migrationServiceStatus = await getSqlMigrationService(
this._model._azureAccount,
subscription,
resourceGroup,
location,
this._createdMigrationService.name);
break; break;
} catch (e) { } catch (e) {
this._dialogObject.message = { this._dialogObject.message = {
@@ -502,7 +514,13 @@ export class CreateSqlMigrationServiceDialog {
} }
await new Promise(r => setTimeout(r, 5000)); await new Promise(r => setTimeout(r, 5000));
} }
const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService!.name, this._model._sessionId); const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData(
this._model._azureAccount,
subscription,
resourceGroup,
location,
this._createdMigrationService!.name);
this.irNodes = migrationServiceMonitoringStatus.nodes.map((node) => { this.irNodes = migrationServiceMonitoringStatus.nodes.map((node) => {
return node.nodeName; return node.nodeName;
}); });
@@ -536,7 +554,12 @@ export class CreateSqlMigrationServiceDialog {
const subscription = this._model._targetSubscription; const subscription = this._model._targetSubscription;
const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name; const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name;
const location = this._model._targetServerInstance.location; const location = this._model._targetServerInstance.location;
const keys = await getSqlMigrationServiceAuthKeys(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService!.name, this._model._sessionId); const keys = await getSqlMigrationServiceAuthKeys(
this._model._azureAccount,
subscription,
resourceGroup,
location,
this._createdMigrationService!.name);
this._copyKey1Button = this._view.modelBuilder.button().withProps({ this._copyKey1Button = this._view.modelBuilder.button().withProps({
title: constants.COPY_KEY1, title: constants.COPY_KEY1,

View File

@@ -7,10 +7,11 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel'; import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
import * as constants from '../../constants/strings'; import * as constants from '../../constants/strings';
import { SqlManagedInstance } from '../../api/azure'; import { getMigrationTargetInstance, SqlManagedInstance } from '../../api/azure';
import { IconPathHelper } from '../../constants/iconPathHelper'; import { IconPathHelper } from '../../constants/iconPathHelper';
import { convertByteSizeToReadableUnit, get12HourTime } from '../../api/utils'; import { convertByteSizeToReadableUnit, get12HourTime } from '../../api/utils';
import * as styles from '../../constants/styles'; import * as styles from '../../constants/styles';
import { isBlobMigration } from '../../constants/helper';
export class ConfirmCutoverDialog { export class ConfirmCutoverDialog {
private _dialogObject!: azdata.window.Dialog; private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView; private _view!: azdata.ModelView;
@@ -21,20 +22,17 @@ export class ConfirmCutoverDialog {
} }
async initialize(): Promise<void> { async initialize(): Promise<void> {
const tab = azdata.window.createTab('');
let tab = azdata.window.createTab('');
tab.registerContent(async (view: azdata.ModelView) => { tab.registerContent(async (view: azdata.ModelView) => {
this._view = view; this._view = view;
const completeCutoverText = view.modelBuilder.text().withProps({ const completeCutoverText = view.modelBuilder.text().withProps({
value: constants.COMPLETE_CUTOVER, value: constants.COMPLETE_CUTOVER,
CSSStyles: { CSSStyles: { ...styles.PAGE_TITLE_CSS }
...styles.PAGE_TITLE_CSS
}
}).component(); }).component();
const sourceDatabaseText = view.modelBuilder.text().withProps({ const sourceDatabaseText = view.modelBuilder.text().withProps({
value: this.migrationCutoverModel._migration.migrationContext.properties.sourceDatabaseName, value: this.migrationCutoverModel._migration.properties.sourceDatabaseName,
CSSStyles: { CSSStyles: {
...styles.SMALL_NOTE_CSS, ...styles.SMALL_NOTE_CSS,
'margin': '4px 0px 8px' 'margin': '4px 0px 8px'
@@ -42,12 +40,9 @@ export class ConfirmCutoverDialog {
}).component(); }).component();
const separator = this._view.modelBuilder.separator().withProps({ width: '800px' }).component(); const separator = this._view.modelBuilder.separator().withProps({ width: '800px' }).component();
const helpMainText = this._view.modelBuilder.text().withProps({ const helpMainText = this._view.modelBuilder.text().withProps({
value: constants.CUTOVER_HELP_MAIN, value: constants.CUTOVER_HELP_MAIN,
CSSStyles: { CSSStyles: { ...styles.BODY_CSS }
...styles.BODY_CSS
}
}).component(); }).component();
const helpStepsText = this._view.modelBuilder.text().withProps({ const helpStepsText = this._view.modelBuilder.text().withProps({
@@ -58,8 +53,9 @@ export class ConfirmCutoverDialog {
} }
}).component(); }).component();
const fileContainer = isBlobMigration(this.migrationCutoverModel.migrationStatus)
const fileContainer = this.migrationCutoverModel.isBlobMigration() ? this.createBlobFileContainer() : this.createNetworkShareFileContainer(); ? this.createBlobFileContainer()
: this.createNetworkShareFileContainer();
const confirmCheckbox = this._view.modelBuilder.checkBox().withProps({ const confirmCheckbox = this._view.modelBuilder.checkBox().withProps({
CSSStyles: { CSSStyles: {
@@ -76,16 +72,19 @@ export class ConfirmCutoverDialog {
const cutoverWarning = this._view.modelBuilder.infoBox().withProps({ const cutoverWarning = this._view.modelBuilder.infoBox().withProps({
text: constants.COMPLETING_CUTOVER_WARNING, text: constants.COMPLETING_CUTOVER_WARNING,
style: 'warning', style: 'warning',
CSSStyles: { CSSStyles: { ...styles.BODY_CSS }
...styles.BODY_CSS
}
}).component(); }).component();
let infoDisplay = 'none'; let infoDisplay = 'none';
if (this.migrationCutoverModel._migration.targetManagedInstance.id.toLocaleLowerCase().includes('managedinstances') if (this.migrationCutoverModel._migration.id.toLocaleLowerCase().includes('managedinstances')) {
&& (<SqlManagedInstance>this.migrationCutoverModel._migration.targetManagedInstance)?.sku?.tier === 'BusinessCritical') { const targetInstance = await getMigrationTargetInstance(
infoDisplay = 'inline'; this.migrationCutoverModel._serviceConstext.azureAccount!,
this.migrationCutoverModel._serviceConstext.subscription!,
this.migrationCutoverModel._migration);
if ((<SqlManagedInstance>targetInstance)?.sku?.tier === 'BusinessCritical') {
infoDisplay = 'inline';
}
} }
const businessCriticalInfoBox = this._view.modelBuilder.infoBox().withProps({ const businessCriticalInfoBox = this._view.modelBuilder.infoBox().withProps({
@@ -111,23 +110,18 @@ export class ConfirmCutoverDialog {
businessCriticalInfoBox businessCriticalInfoBox
]).component(); ]).component();
this._dialogObject.okButton.enabled = false; this._dialogObject.okButton.enabled = false;
this._dialogObject.okButton.label = constants.COMPLETE_CUTOVER; this._dialogObject.okButton.label = constants.COMPLETE_CUTOVER;
this._disposables.push(this._dialogObject.okButton.onClick(async (e) => { this._disposables.push(this._dialogObject.okButton.onClick(async (e) => {
await this.migrationCutoverModel.startCutover(); await this.migrationCutoverModel.startCutover();
void vscode.window.showInformationMessage(constants.CUTOVER_IN_PROGRESS(this.migrationCutoverModel._migration.migrationContext.properties.sourceDatabaseName)); void vscode.window.showInformationMessage(
constants.CUTOVER_IN_PROGRESS(
this.migrationCutoverModel._migration.properties.sourceDatabaseName));
})); }));
const formBuilder = view.modelBuilder.formContainer().withFormItems( const formBuilder = view.modelBuilder.formContainer().withFormItems(
[ [{ component: container }],
{ { horizontal: false }
component: container
}
],
{
horizontal: false
}
); );
const form = formBuilder.withLayout({ width: '100%' }).component(); const form = formBuilder.withLayout({ width: '100%' }).component();
@@ -144,18 +138,14 @@ export class ConfirmCutoverDialog {
private createBlobFileContainer(): azdata.FlexContainer { private createBlobFileContainer(): azdata.FlexContainer {
const container = this._view.modelBuilder.flexContainer().withProps({ const container = this._view.modelBuilder.flexContainer().withProps({
CSSStyles: { CSSStyles: { 'margin': '8px 0' }
'margin': '8px 0'
}
}).component(); }).component();
const containerHeading = this._view.modelBuilder.text().withProps({ const containerHeading = this._view.modelBuilder.text().withProps({
value: constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0), value: constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0),
width: 250, width: 250,
CSSStyles: { CSSStyles: { ...styles.LABEL_CSS }
...styles.LABEL_CSS
}
}).component(); }).component();
container.addItem(containerHeading, { flex: '0' });
const refreshButton = this._view.modelBuilder.button().withProps({ const refreshButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.refresh, iconPath: IconPathHelper.refresh,
@@ -165,13 +155,7 @@ export class ConfirmCutoverDialog {
height: 20, height: 20,
label: constants.REFRESH, label: constants.REFRESH,
}).component(); }).component();
this._disposables.push(refreshButton.onDidClick(async e => {
container.addItem(containerHeading, {
flex: '0'
});
refreshButton.onDidClick(async e => {
refreshLoader.loading = true; refreshLoader.loading = true;
try { try {
await this.migrationCutoverModel.fetchStatus(); await this.migrationCutoverModel.fetchStatus();
@@ -184,11 +168,8 @@ export class ConfirmCutoverDialog {
} finally { } finally {
refreshLoader.loading = false; refreshLoader.loading = false;
} }
}); }));
container.addItem(refreshButton, { flex: '0' });
container.addItem(refreshButton, {
flex: '0'
});
const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({ const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
loading: false, loading: false,
@@ -197,10 +178,8 @@ export class ConfirmCutoverDialog {
'margin-left': '8px' 'margin-left': '8px'
} }
}).component(); }).component();
container.addItem(refreshLoader, { flex: '0' });
container.addItem(refreshLoader, {
flex: '0'
});
return container; return container;
} }
@@ -227,23 +206,18 @@ export class ConfirmCutoverDialog {
} }
}).component(); }).component();
containerHeading.onDidClick(async e => { this._disposables.push(containerHeading.onDidClick(async e => {
if (expanded) { if (expanded) {
containerHeading.iconPath = IconPathHelper.expandButtonClosed; containerHeading.iconPath = IconPathHelper.expandButtonClosed;
containerHeading.iconHeight = 12; containerHeading.iconHeight = 12;
await fileTable.updateCssStyles({ await fileTable.updateCssStyles({ 'display': 'none' });
'display': 'none'
});
} else { } else {
containerHeading.iconPath = IconPathHelper.expandButtonOpen; containerHeading.iconPath = IconPathHelper.expandButtonOpen;
containerHeading.iconHeight = 8; containerHeading.iconHeight = 8;
await fileTable.updateCssStyles({ await fileTable.updateCssStyles({ 'display': 'inline' });
'display': 'inline'
});
} }
expanded = !expanded; expanded = !expanded;
}); }));
const refreshButton = this._view.modelBuilder.button().withProps({ const refreshButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.refresh, iconPath: IconPathHelper.refresh,
@@ -252,16 +226,12 @@ export class ConfirmCutoverDialog {
width: 70, width: 70,
height: 20, height: 20,
label: constants.REFRESH, label: constants.REFRESH,
CSSStyles: { CSSStyles: { 'margin-top': '13px' }
'margin-top': '13px'
}
}).component(); }).component();
headingRow.addItem(containerHeading, { headingRow.addItem(containerHeading, { flex: '0' });
flex: '0'
});
refreshButton.onDidClick(async e => { this._disposables.push(refreshButton.onDidClick(async e => {
refreshLoader.loading = true; refreshLoader.loading = true;
try { try {
await this.migrationCutoverModel.fetchStatus(); await this.migrationCutoverModel.fetchStatus();
@@ -276,11 +246,8 @@ export class ConfirmCutoverDialog {
} finally { } finally {
refreshLoader.loading = false; refreshLoader.loading = false;
} }
}); }));
headingRow.addItem(refreshButton, { flex: '0' });
headingRow.addItem(refreshButton, {
flex: '0'
});
const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({ const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
loading: false, loading: false,
@@ -290,20 +257,13 @@ export class ConfirmCutoverDialog {
'height': '13px' 'height': '13px'
} }
}).component(); }).component();
headingRow.addItem(refreshLoader, { flex: '0' });
headingRow.addItem(refreshLoader, {
flex: '0'
});
container.addItem(headingRow); container.addItem(headingRow);
const lastScanCompleted = this._view.modelBuilder.text().withProps({ const lastScanCompleted = this._view.modelBuilder.text().withProps({
value: constants.LAST_SCAN_COMPLETED(get12HourTime(new Date())), value: constants.LAST_SCAN_COMPLETED(get12HourTime(new Date())),
CSSStyles: { CSSStyles: { ...styles.NOTE_CSS }
...styles.NOTE_CSS
}
}).component(); }).component();
container.addItem(lastScanCompleted); container.addItem(lastScanCompleted);
const fileTable = this._view.modelBuilder.table().withProps({ const fileTable = this._view.modelBuilder.table().withProps({
@@ -327,9 +287,7 @@ export class ConfirmCutoverDialog {
data: [], data: [],
width: 400, width: 400,
height: 150, height: 150,
CSSStyles: { CSSStyles: { 'display': 'none' }
'display': 'none'
}
}).component(); }).component();
container.addItem(fileTable); container.addItem(fileTable);
this.refreshFileTable(fileTable); this.refreshFileTable(fileTable);
@@ -347,9 +305,7 @@ export class ConfirmCutoverDialog {
]; ];
}); });
} else { } else {
fileTable.data = [ fileTable.data = [[constants.NO_PENDING_BACKUPS]];
[constants.NO_PENDING_BACKUPS]
];
} }
} }

View File

@@ -6,26 +6,24 @@
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { IconPathHelper } from '../../constants/iconPathHelper'; import { IconPathHelper } from '../../constants/iconPathHelper';
import { BackupFileInfoStatus, MigrationContext, MigrationStatus } from '../../models/migrationLocalStorage'; import { BackupFileInfoStatus, MigrationServiceContext, MigrationStatus } from '../../models/migrationLocalStorage';
import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel'; import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
import * as loc from '../../constants/strings'; import * as loc from '../../constants/strings';
import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage, SupportedAutoRefreshIntervals, clearDialogMessage, displayDialogErrorMessage } from '../../api/utils'; import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage, clearDialogMessage, displayDialogErrorMessage } from '../../api/utils';
import { EOL } from 'os'; import { EOL } from 'os';
import { ConfirmCutoverDialog } from './confirmCutoverDialog'; import { ConfirmCutoverDialog } from './confirmCutoverDialog';
import { logError, TelemetryViews } from '../../telemtery'; import { logError, TelemetryViews } from '../../telemtery';
import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog'; import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog';
import * as styles from '../../constants/styles'; import * as styles from '../../constants/styles';
import { canRetryMigration } from '../../constants/helper'; import { canRetryMigration, getMigrationStatus, isBlobMigration, isOfflineMigation } from '../../constants/helper';
import { DatabaseMigration, getResourceName } from '../../api/azure';
const refreshFrequency: SupportedAutoRefreshIntervals = 30000;
const statusImageSize: number = 14; const statusImageSize: number = 14;
export class MigrationCutoverDialog { export class MigrationCutoverDialog {
private _context: vscode.ExtensionContext;
private _dialogObject!: azdata.window.Dialog; private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView; private _view!: azdata.ModelView;
private _model: MigrationCutoverDialogModel; private _model: MigrationCutoverDialogModel;
private _migration: MigrationContext;
private _databaseTitleName!: azdata.TextComponent; private _databaseTitleName!: azdata.TextComponent;
private _cutoverButton!: azdata.ButtonComponent; private _cutoverButton!: azdata.ButtonComponent;
@@ -52,7 +50,6 @@ export class MigrationCutoverDialog {
private _fileCount!: azdata.TextComponent; private _fileCount!: azdata.TextComponent;
private _fileTable!: azdata.DeclarativeTableComponent; private _fileTable!: azdata.DeclarativeTableComponent;
private _autoRefreshHandle!: any;
private _disposables: vscode.Disposable[] = []; private _disposables: vscode.Disposable[] = [];
private _emptyTableFill!: azdata.FlexContainer; private _emptyTableFill!: azdata.FlexContainer;
@@ -60,10 +57,13 @@ export class MigrationCutoverDialog {
readonly _infoFieldWidth: string = '250px'; readonly _infoFieldWidth: string = '250px';
constructor(context: vscode.ExtensionContext, migration: MigrationContext) { constructor(
this._context = context; private readonly _context: vscode.ExtensionContext,
this._migration = migration; private readonly _serviceContext: MigrationServiceContext,
this._model = new MigrationCutoverDialogModel(migration); private readonly _migration: DatabaseMigration,
private readonly _onClosedCallback: () => Promise<void>) {
this._model = new MigrationCutoverDialogModel(_serviceContext, _migration);
this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 'wide'); this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 'wide');
} }
@@ -224,15 +224,14 @@ export class MigrationCutoverDialog {
); );
const form = formBuilder.withLayout({ width: '100%' }).component(); const form = formBuilder.withLayout({ width: '100%' }).component();
this._disposables.push(this._view.onClosed(e => { this._disposables.push(
clearInterval(this._autoRefreshHandle); this._view.onClosed(e => {
this._disposables.forEach( this._disposables.forEach(
d => { try { d.dispose(); } catch { } }); d => { try { d.dispose(); } catch { } });
})); }));
return view.initializeModel(form).then(async (value) => { await view.initializeModel(form);
await this.refreshStatus(); await this.refreshStatus();
});
} catch (e) { } catch (e) {
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e); logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
} }
@@ -242,9 +241,6 @@ export class MigrationCutoverDialog {
this._dialogObject.cancelButton.hidden = true; this._dialogObject.cancelButton.hidden = true;
this._dialogObject.okButton.label = loc.CLOSE; this._dialogObject.okButton.label = loc.CLOSE;
this._disposables.push(this._dialogObject.okButton.onClick(e => {
clearInterval(this._autoRefreshHandle);
}));
azdata.window.openDialog(this._dialogObject); azdata.window.openDialog(this._dialogObject);
} }
@@ -262,7 +258,7 @@ export class MigrationCutoverDialog {
...styles.PAGE_TITLE_CSS ...styles.PAGE_TITLE_CSS
}, },
width: 950, width: 950,
value: this._model._migration.migrationContext.properties.sourceDatabaseName value: this._model._migration.properties.sourceDatabaseName
}).component(); }).component();
const databaseSubTitle = this._view.modelBuilder.text().withProps({ const databaseSubTitle = this._view.modelBuilder.text().withProps({
@@ -282,8 +278,6 @@ export class MigrationCutoverDialog {
width: 950 width: 950
}).component(); }).component();
this.setAutoRefresh(refreshFrequency);
const titleLogoContainer = this._view.modelBuilder.flexContainer().withProps({ const titleLogoContainer = this._view.modelBuilder.flexContainer().withProps({
width: 1000 width: 1000
}).component(); }).component();
@@ -314,7 +308,7 @@ export class MigrationCutoverDialog {
enabled: false, enabled: false,
CSSStyles: { CSSStyles: {
...styles.BODY_CSS, ...styles.BODY_CSS,
'display': this._isOnlineMigration() ? 'block' : 'none' 'display': isOfflineMigation(this._model._migration) ? 'none' : 'block'
} }
}).component(); }).component();
@@ -322,16 +316,13 @@ export class MigrationCutoverDialog {
await this.refreshStatus(); await this.refreshStatus();
const dialog = new ConfirmCutoverDialog(this._model); const dialog = new ConfirmCutoverDialog(this._model);
await dialog.initialize(); await dialog.initialize();
await this.refreshStatus();
if (this._model.CutoverError) { if (this._model.CutoverError) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CUTOVER_ERROR, this._model.CutoverError); displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CUTOVER_ERROR, this._model.CutoverError);
} }
})); }));
headerActions.addItem(this._cutoverButton, { headerActions.addItem(this._cutoverButton, { flex: '0' });
flex: '0'
});
this._cancelButton = this._view.modelBuilder.button().withProps({ this._cancelButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.cancel, iconPath: IconPathHelper.cancel,
@@ -377,7 +368,11 @@ export class MigrationCutoverDialog {
this._disposables.push(this._retryButton.onDidClick( this._disposables.push(this._retryButton.onDidClick(
async (e) => { async (e) => {
await this.refreshStatus(); await this.refreshStatus();
let retryMigrationDialog = new RetryMigrationDialog(this._context, this._migration); const retryMigrationDialog = new RetryMigrationDialog(
this._context,
this._serviceContext,
this._migration,
this._onClosedCallback);
await retryMigrationDialog.openDialog(); await retryMigrationDialog.openDialog();
} }
)); ));
@@ -397,12 +392,14 @@ export class MigrationCutoverDialog {
} }
}).component(); }).component();
this._disposables.push(this._refreshButton.onDidClick( this._disposables.push(
async (e) => await this.refreshStatus())); this._refreshButton.onDidClick(async (e) => {
this._refreshButton.enabled = false;
await this.refreshStatus();
this._refreshButton.enabled = true;
}));
headerActions.addItem(this._refreshButton, { headerActions.addItem(this._refreshButton, { flex: '0' });
flex: '0',
});
this._copyDatabaseMigrationDetails = this._view.modelBuilder.button().withProps({ this._copyDatabaseMigrationDetails = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.copy, iconPath: IconPathHelper.copy,
@@ -425,9 +422,7 @@ export class MigrationCutoverDialog {
headerActions.addItem(this._copyDatabaseMigrationDetails, { headerActions.addItem(this._copyDatabaseMigrationDetails, {
flex: '0', flex: '0',
CSSStyles: { CSSStyles: { 'margin-left': '5px' }
'margin-left': '5px'
}
}); });
// create new support request button. Hiding button until sql migration support has been setup. // create new support request button. Hiding button until sql migration support has been setup.
@@ -443,11 +438,11 @@ export class MigrationCutoverDialog {
} }
}).component(); }).component();
this._newSupportRequest.onDidClick(async (e) => { this._disposables.push(this._newSupportRequest.onDidClick(async (e) => {
const serviceId = this._model._migration.controller.id; const serviceId = this._model._migration.properties.migrationService;
const supportUrl = `https://portal.azure.com/#resource${serviceId}/supportrequest`; const supportUrl = `https://portal.azure.com/#resource${serviceId}/supportrequest`;
await vscode.env.openExternal(vscode.Uri.parse(supportUrl)); await vscode.env.openExternal(vscode.Uri.parse(supportUrl));
}); }));
headerActions.addItem(this._newSupportRequest, { headerActions.addItem(this._newSupportRequest, {
flex: '0', flex: '0',
@@ -519,12 +514,12 @@ export class MigrationCutoverDialog {
addInfoFieldToContainer(this._targetServerInfoField, flexTarget); addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget); addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
const isBlobMigration = this._model.isBlobMigration(); const _isBlobMigration = isBlobMigration(this._model._migration);
const flexStatus = this._view.modelBuilder.flexContainer().withLayout({ const flexStatus = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column' flexFlow: 'column'
}).component(); }).component();
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' '); this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', isBlobMigration); this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', _isBlobMigration);
this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, ''); this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus); addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
addInfoFieldToContainer(this._fullBackupFileOnInfoField, flexStatus); addInfoFieldToContainer(this._fullBackupFileOnInfoField, flexStatus);
@@ -533,10 +528,10 @@ export class MigrationCutoverDialog {
const flexFile = this._view.modelBuilder.flexContainer().withLayout({ const flexFile = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column' flexFlow: 'column'
}).component(); }).component();
this._lastLSNInfoField = await this.createInfoField(loc.LAST_APPLIED_LSN, '', isBlobMigration); this._lastLSNInfoField = await this.createInfoField(loc.LAST_APPLIED_LSN, '', _isBlobMigration);
this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, ''); this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', isBlobMigration); this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', _isBlobMigration);
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', !isBlobMigration); this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', !_isBlobMigration);
addInfoFieldToContainer(this._lastLSNInfoField, flexFile); addInfoFieldToContainer(this._lastLSNInfoField, flexFile);
addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile); addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
addInfoFieldToContainer(this._lastAppliedBackupTakenOnInfoField, flexFile); addInfoFieldToContainer(this._lastAppliedBackupTakenOnInfoField, flexFile);
@@ -561,33 +556,8 @@ export class MigrationCutoverDialog {
return flexInfo; return flexInfo;
} }
private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void {
const shouldRefresh = (status: string | undefined) => !status
|| status === MigrationStatus.InProgress
|| status === MigrationStatus.Creating
|| status === MigrationStatus.Completing
|| status === MigrationStatus.Canceling;
if (shouldRefresh(this.getMigrationStatus())) {
const classVariable = this;
clearInterval(this._autoRefreshHandle);
if (interval !== -1) {
this._autoRefreshHandle = setInterval(async function () { await classVariable.refreshStatus(); }, interval);
}
}
}
private getMigrationDetails(): string { private getMigrationDetails(): string {
if (this._model.migrationOpStatus) { return JSON.stringify(this._model.migrationStatus, undefined, 2);
return (JSON.stringify(
{
'async-operation-details': this._model.migrationOpStatus,
'details': this._model.migrationStatus
}
, undefined, 2));
} else {
return (JSON.stringify(this._model.migrationStatus, undefined, 2));
}
} }
private async refreshStatus(): Promise<void> { private async refreshStatus(): Promise<void> {
@@ -598,18 +568,13 @@ export class MigrationCutoverDialog {
try { try {
clearDialogMessage(this._dialogObject); clearDialogMessage(this._dialogObject);
if (this._isOnlineMigration()) { await this._cutoverButton.updateCssStyles(
await this._cutoverButton.updateCssStyles({ { 'display': isOfflineMigation(this._model._migration) ? 'none' : 'block' });
'display': 'block'
});
}
this.isRefreshing = true; this.isRefreshing = true;
this._refreshLoader.loading = true; this._refreshLoader.loading = true;
await this._model.fetchStatus(); await this._model.fetchStatus();
const errors = []; const errors = [];
errors.push(this._model.migrationOpStatus.error?.message);
errors.push(this._model._migration.asyncOperationResult?.error?.message);
errors.push(this._model.migrationStatus.properties.provisioningError); errors.push(this._model.migrationStatus.properties.provisioningError);
errors.push(this._model.migrationStatus.properties.migrationFailureError?.message); errors.push(this._model.migrationStatus.properties.migrationFailureError?.message);
errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []); errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []);
@@ -626,12 +591,12 @@ export class MigrationCutoverDialog {
description: this.getMigrationDetails() description: this.getMigrationDetails()
}; };
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId); const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
const sqlServerName = this._model._migration.sourceConnectionProfile.serverName; const sqlServerName = this._model._migration.properties.sourceServerName;
const sourceDatabaseName = this._model._migration.migrationContext.properties.sourceDatabaseName; const sourceDatabaseName = this._model._migration.properties.sourceDatabaseName;
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!); const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion; const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
const targetDatabaseName = this._model._migration.migrationContext.name; const targetDatabaseName = this._model._migration.name;
const targetServerName = this._model._migration.targetManagedInstance.name; const targetServerName = getResourceName(this._model._migration.id);
let targetServerVersion; let targetServerVersion;
if (this._model.migrationStatus.id.includes('managedInstances')) { if (this._model.migrationStatus.id.includes('managedInstances')) {
targetServerVersion = loc.AZURE_SQL_DATABASE_MANAGED_INSTANCE; targetServerVersion = loc.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
@@ -644,30 +609,30 @@ export class MigrationCutoverDialog {
const tableData: ActiveBackupFileSchema[] = []; const tableData: ActiveBackupFileSchema[] = [];
this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach((activeBackupSet) => { 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 (this._shouldDisplayBackupFileTable()) { if (activeBackupSet.listOfBackupFiles[0].fileName === this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
tableData.push( lastAppliedSSN = activeBackupSet.lastLSN;
...activeBackupSet.listOfBackupFiles.map(f => { lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
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._sourceDatabaseInfoField.text.value = sourceDatabaseName;
this._sourceDetailsInfoField.text.value = sqlServerName; this._sourceDetailsInfoField.text.value = sqlServerName;
@@ -677,21 +642,23 @@ export class MigrationCutoverDialog {
this._targetServerInfoField.text.value = targetServerName; this._targetServerInfoField.text.value = targetServerName;
this._targetVersionInfoField.text.value = targetServerVersion; this._targetVersionInfoField.text.value = targetServerVersion;
const migrationStatusTextValue = this.getMigrationStatus(); const migrationStatusTextValue = this._getMigrationStatus();
this._migrationStatusInfoField.text.value = migrationStatusTextValue ?? '-'; this._migrationStatusInfoField.text.value = migrationStatusTextValue ?? '-';
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migrationStatusTextValue); this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migrationStatusTextValue);
this._fullBackupFileOnInfoField.text.value = this._model.migrationStatus?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? '-'; this._fullBackupFileOnInfoField.text.value = this._model.migrationStatus?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? '-';
let backupLocation; let backupLocation;
const isBlobMigration = this._model.isBlobMigration(); const _isBlobMigration = isBlobMigration(this._model._migration);
// Displaying storage accounts and blob container for azure blob backups. // Displaying storage accounts and blob container for azure blob backups.
if (isBlobMigration) { if (_isBlobMigration) {
const storageAccountResourceId = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId; const storageAccountResourceId = this._model._migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId;
const blobContainerName = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName; const blobContainerName = this._model._migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName;
backupLocation = `${storageAccountResourceId?.split('/').pop()} - ${blobContainerName}`; backupLocation = storageAccountResourceId && blobContainerName
? `${storageAccountResourceId?.split('/').pop()} - ${blobContainerName}`
: undefined;
} else { } else {
const fileShare = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.fileShare; const fileShare = this._model._migration.properties.backupConfiguration?.sourceLocation?.fileShare;
backupLocation = fileShare?.path! ?? '-'; backupLocation = fileShare?.path! ?? '-';
} }
this._backupLocationInfoField.text.value = backupLocation ?? '-'; this._backupLocationInfoField.text.value = backupLocation ?? '-';
@@ -700,7 +667,7 @@ export class MigrationCutoverDialog {
this._lastAppliedBackupInfoField.text.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename ?? '-'; this._lastAppliedBackupInfoField.text.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename ?? '-';
this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : '-'; this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : '-';
if (isBlobMigration) { if (_isBlobMigration) {
if (!this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) { if (!this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) {
this._currentRestoringFileInfoField.text.value = '-'; this._currentRestoringFileInfoField.text.value = '-';
} else if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename === this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) { } else if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename === this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) {
@@ -752,7 +719,7 @@ export class MigrationCutoverDialog {
this._cutoverButton.enabled = false; this._cutoverButton.enabled = false;
if (migrationStatusTextValue === MigrationStatus.InProgress) { if (migrationStatusTextValue === MigrationStatus.InProgress) {
if (isBlobMigration) { if (_isBlobMigration) {
if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) { if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
this._cutoverButton.enabled = true; this._cutoverButton.enabled = true;
} }
@@ -856,21 +823,14 @@ export class MigrationCutoverDialog {
}; };
} }
private _isOnlineMigration(): boolean {
return this._model._migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? false : true;
}
private _shouldDisplayBackupFileTable(): boolean { private _shouldDisplayBackupFileTable(): boolean {
return !this._model.isBlobMigration(); return !isBlobMigration(this._model._migration);
} }
private getMigrationStatus(): string { private _getMigrationStatus(): string {
if (this._model.migrationStatus) { return this._model.migrationStatus
return this._model.migrationStatus.properties.migrationStatus ? getMigrationStatus(this._model.migrationStatus)
?? this._model.migrationStatus.properties.provisioningState; : getMigrationStatus(this._model._migration);
}
return this._model._migration.migrationContext.properties.migrationStatus
?? this._model._migration.migrationContext.properties.provisioningState;
} }
} }

View File

@@ -3,43 +3,36 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { getMigrationStatus, DatabaseMigration, startMigrationCutover, stopMigration, getMigrationAsyncOperationDetails, AzureAsyncOperationResource, BackupFileInfo, getResourceGroupFromId } from '../../api/azure'; import { DatabaseMigration, startMigrationCutover, stopMigration, BackupFileInfo, getResourceGroupFromId, getMigrationDetails, getMigrationTargetName } from '../../api/azure';
import { BackupFileInfoStatus, MigrationContext } from '../../models/migrationLocalStorage'; import { BackupFileInfoStatus, MigrationServiceContext } from '../../models/migrationLocalStorage';
import { logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../../telemtery'; import { logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../../telemtery';
import * as constants from '../../constants/strings'; import * as constants from '../../constants/strings';
import { EOL } from 'os'; import { EOL } from 'os';
import { getMigrationTargetType, getMigrationMode } from '../../constants/helper'; import { getMigrationTargetType, getMigrationMode, isBlobMigration } from '../../constants/helper';
export class MigrationCutoverDialogModel { export class MigrationCutoverDialogModel {
public CutoverError?: Error; public CutoverError?: Error;
public CancelMigrationError?: Error; public CancelMigrationError?: Error;
public migrationStatus!: DatabaseMigration; public migrationStatus!: DatabaseMigration;
public migrationOpStatus!: AzureAsyncOperationResource;
constructor(public _migration: MigrationContext) {
constructor(
public _serviceConstext: MigrationServiceContext,
public _migration: DatabaseMigration
) {
} }
public async fetchStatus(): Promise<void> { public async fetchStatus(): Promise<void> {
if (this._migration.asyncUrl) { this.migrationStatus = await getMigrationDetails(
this.migrationOpStatus = await getMigrationAsyncOperationDetails( this._serviceConstext.azureAccount!,
this._migration.azureAccount, this._serviceConstext.subscription!,
this._migration.subscription, this._migration.id,
this._migration.asyncUrl, this._migration.properties?.migrationOperationId);
this._migration.sessionId!);
}
this.migrationStatus = await getMigrationStatus(
this._migration.azureAccount,
this._migration.subscription,
this._migration.migrationContext,
this._migration.sessionId!);
sendSqlMigrationActionEvent( sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog, TelemetryViews.MigrationCutoverDialog,
TelemetryAction.MigrationStatus, TelemetryAction.MigrationStatus,
{ {
'sessionId': this._migration.sessionId!,
'migrationStatus': this.migrationStatus.properties?.migrationStatus 'migrationStatus': this.migrationStatus.properties?.migrationStatus
}, },
{} {}
@@ -51,18 +44,16 @@ export class MigrationCutoverDialogModel {
public async startCutover(): Promise<DatabaseMigration | undefined> { public async startCutover(): Promise<DatabaseMigration | undefined> {
try { try {
this.CutoverError = undefined; this.CutoverError = undefined;
if (this.migrationStatus) { if (this._migration) {
const cutover = await startMigrationCutover( const cutover = await startMigrationCutover(
this._migration.azureAccount, this._serviceConstext.azureAccount!,
this._migration.subscription, this._serviceConstext.subscription!,
this.migrationStatus, this._migration!);
this._migration.sessionId!
);
sendSqlMigrationActionEvent( sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog, TelemetryViews.MigrationCutoverDialog,
TelemetryAction.CutoverMigration, TelemetryAction.CutoverMigration,
{ {
...this.getTelemetryProps(this._migration), ...this.getTelemetryProps(this._serviceConstext, this._migration),
'migrationEndTime': new Date().toString(), 'migrationEndTime': new Date().toString(),
}, },
{} {}
@@ -79,8 +70,6 @@ export class MigrationCutoverDialogModel {
public async fetchErrors(): Promise<string> { public async fetchErrors(): Promise<string> {
const errors = []; const errors = [];
await this.fetchStatus(); await this.fetchStatus();
errors.push(this.migrationOpStatus.error?.message);
errors.push(this._migration.asyncOperationResult?.error?.message);
errors.push(this.migrationStatus.properties.migrationFailureError?.message); errors.push(this.migrationStatus.properties.migrationFailureError?.message);
return errors return errors
.filter((e, i, arr) => e !== undefined && i === arr.indexOf(e)) .filter((e, i, arr) => e !== undefined && i === arr.indexOf(e))
@@ -93,18 +82,16 @@ export class MigrationCutoverDialogModel {
if (this.migrationStatus) { if (this.migrationStatus) {
const cutoverStartTime = new Date().toString(); const cutoverStartTime = new Date().toString();
await stopMigration( await stopMigration(
this._migration.azureAccount, this._serviceConstext.azureAccount!,
this._migration.subscription, this._serviceConstext.subscription!,
this.migrationStatus, this.migrationStatus);
this._migration.sessionId!
);
sendSqlMigrationActionEvent( sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog, TelemetryViews.MigrationCutoverDialog,
TelemetryAction.CancelMigration, TelemetryAction.CancelMigration,
{ {
...this.getTelemetryProps(this._migration), ...this.getTelemetryProps(this._serviceConstext, this._migration),
'migrationMode': getMigrationMode(this._migration), 'migrationMode': getMigrationMode(this._migration),
'cutoverStartTime': cutoverStartTime 'cutoverStartTime': cutoverStartTime,
}, },
{} {}
); );
@@ -116,12 +103,8 @@ export class MigrationCutoverDialogModel {
return undefined!; return undefined!;
} }
public isBlobMigration(): boolean {
return this._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.azureBlob !== undefined;
}
public confirmCutoverStepsString(): string { public confirmCutoverStepsString(): string {
if (this.isBlobMigration()) { if (isBlobMigration(this.migrationStatus)) {
return `${constants.CUTOVER_HELP_STEP1} return `${constants.CUTOVER_HELP_STEP1}
${constants.CUTOVER_HELP_STEP2_BLOB_CONTAINER} ${constants.CUTOVER_HELP_STEP2_BLOB_CONTAINER}
${constants.CUTOVER_HELP_STEP3_BLOB_CONTAINER}`; ${constants.CUTOVER_HELP_STEP3_BLOB_CONTAINER}`;
@@ -152,16 +135,15 @@ export class MigrationCutoverDialogModel {
return files; return files;
} }
private getTelemetryProps(migration: MigrationContext) { private getTelemetryProps(serviceContext: MigrationServiceContext, migration: DatabaseMigration) {
return { return {
'sessionId': migration.sessionId!, 'subscriptionId': serviceContext.subscription!.id,
'subscriptionId': migration.subscription.id, 'resourceGroup': getResourceGroupFromId(migration.id),
'resourceGroup': getResourceGroupFromId(migration.targetManagedInstance.id), 'sqlServerName': migration.properties.sourceServerName,
'sqlServerName': migration.sourceConnectionProfile.serverName, 'sourceDatabaseName': migration.properties.sourceDatabaseName,
'sourceDatabaseName': migration.migrationContext.properties.sourceDatabaseName,
'targetType': getMigrationTargetType(migration), 'targetType': getMigrationTargetType(migration),
'targetDatabaseName': migration.migrationContext.name, 'targetDatabaseName': migration.name,
'targetServerName': migration.targetManagedInstance.name, 'targetServerName': getMigrationTargetName(migration),
}; };
} }
} }

View File

@@ -6,22 +6,19 @@
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { IconPathHelper } from '../../constants/iconPathHelper'; import { IconPathHelper } from '../../constants/iconPathHelper';
import { MigrationContext, MigrationLocalStorage, MigrationStatus } from '../../models/migrationLocalStorage'; import { getCurrentMigrations, getSelectedServiceStatus, MigrationLocalStorage, MigrationStatus } from '../../models/migrationLocalStorage';
import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDialog'; import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDialog';
import { AdsMigrationStatus, MigrationStatusDialogModel } from './migrationStatusDialogModel'; import { AdsMigrationStatus, MigrationStatusDialogModel } from './migrationStatusDialogModel';
import * as loc from '../../constants/strings'; import * as loc from '../../constants/strings';
import { clearDialogMessage, convertTimeDifferenceToDuration, displayDialogErrorMessage, filterMigrations, getMigrationStatusImage, SupportedAutoRefreshIntervals } from '../../api/utils'; import { clearDialogMessage, convertTimeDifferenceToDuration, displayDialogErrorMessage, filterMigrations, getMigrationStatusImage } from '../../api/utils';
import { SqlMigrationServiceDetailsDialog } from '../sqlMigrationService/sqlMigrationServiceDetailsDialog'; import { SqlMigrationServiceDetailsDialog } from '../sqlMigrationService/sqlMigrationServiceDetailsDialog';
import { ConfirmCutoverDialog } from '../migrationCutover/confirmCutoverDialog'; import { ConfirmCutoverDialog } from '../migrationCutover/confirmCutoverDialog';
import { MigrationCutoverDialogModel } from '../migrationCutover/migrationCutoverDialogModel'; import { MigrationCutoverDialogModel } from '../migrationCutover/migrationCutoverDialogModel';
import { getMigrationTargetType, getMigrationMode, canRetryMigration } from '../../constants/helper'; import { getMigrationTargetType, getMigrationMode, canRetryMigration } from '../../constants/helper';
import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog'; import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog';
import { DatabaseMigration, getResourceName } from '../../api/azure';
const refreshFrequency: SupportedAutoRefreshIntervals = 180000; import { logError, TelemetryViews } from '../../telemtery';
import { SelectMigrationServiceDialog } from '../selectMigrationService/selectMigrationServiceDialog';
const statusImageSize: number = 14;
const imageCellStyles: azdata.CssStyles = { 'margin': '3px 3px 0 0', 'padding': '0' };
const statusCellStyles: azdata.CssStyles = { 'margin': '0', 'padding': '0' };
const MenuCommands = { const MenuCommands = {
Cutover: 'sqlmigration.cutover', Cutover: 'sqlmigration.cutover',
@@ -40,53 +37,56 @@ export class MigrationStatusDialog {
private _view!: azdata.ModelView; private _view!: azdata.ModelView;
private _searchBox!: azdata.InputBoxComponent; private _searchBox!: azdata.InputBoxComponent;
private _refresh!: azdata.ButtonComponent; private _refresh!: azdata.ButtonComponent;
private _serviceContextButton!: azdata.ButtonComponent;
private _statusDropdown!: azdata.DropDownComponent; private _statusDropdown!: azdata.DropDownComponent;
private _statusTable!: azdata.DeclarativeTableComponent; private _statusTable!: azdata.TableComponent;
private _refreshLoader!: azdata.LoadingComponent; private _refreshLoader!: azdata.LoadingComponent;
private _autoRefreshHandle!: NodeJS.Timeout;
private _disposables: vscode.Disposable[] = []; private _disposables: vscode.Disposable[] = [];
private _filteredMigrations: DatabaseMigration[] = [];
private isRefreshing = false; private isRefreshing = false;
constructor(context: vscode.ExtensionContext, migrations: MigrationContext[], private _filter: AdsMigrationStatus) { constructor(
context: vscode.ExtensionContext,
private _filter: AdsMigrationStatus,
private _onClosedCallback: () => Promise<void>) {
this._context = context; this._context = context;
this._model = new MigrationStatusDialogModel(migrations); this._model = new MigrationStatusDialogModel([]);
this._dialogObject = azdata.window.createModelViewDialog(loc.MIGRATION_STATUS, 'MigrationControllerDialog', 'wide'); this._dialogObject = azdata.window.createModelViewDialog(
loc.MIGRATION_STATUS,
'MigrationControllerDialog',
'wide');
} }
initialize() { async initialize() {
let tab = azdata.window.createTab(''); let tab = azdata.window.createTab('');
tab.registerContent(async (view: azdata.ModelView) => { tab.registerContent(async (view: azdata.ModelView) => {
this._view = view; this._view = view;
this.registerCommands(); this.registerCommands();
const formBuilder = view.modelBuilder.formContainer().withFormItems( const form = view.modelBuilder.formContainer()
[ .withFormItems(
{ [
component: this.createSearchAndRefreshContainer() { component: await this.createSearchAndRefreshContainer() },
}, { component: this.createStatusTable() }
{ ],
component: this.createStatusTable() { horizontal: false }
} ).withLayout({ width: '100%' })
], .component();
{ this._disposables.push(
horizontal: false this._view.onClosed(async e => {
} this._disposables.forEach(
); d => { try { d.dispose(); } catch { } });
const form = formBuilder.withLayout({ width: '100%' }).component();
this._disposables.push(this._view.onClosed(e => {
clearInterval(this._autoRefreshHandle);
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
}));
return view.initializeModel(form); await this._onClosedCallback();
}));
await view.initializeModel(form);
return await this.refreshTable();
}); });
this._dialogObject.content = [tab]; this._dialogObject.content = [tab];
this._dialogObject.cancelButton.hidden = true; this._dialogObject.cancelButton.hidden = true;
this._dialogObject.okButton.label = loc.CLOSE; this._dialogObject.okButton.label = loc.CLOSE;
this._disposables.push(this._dialogObject.okButton.onClick(e => {
clearInterval(this._autoRefreshHandle);
}));
azdata.window.openDialog(this._dialogObject); azdata.window.openDialog(this._dialogObject);
} }
@@ -100,111 +100,124 @@ export class MigrationStatusDialog {
private canCutoverMigration = (status: string | undefined) => status === MigrationStatus.InProgress; private canCutoverMigration = (status: string | undefined) => status === MigrationStatus.InProgress;
private createSearchAndRefreshContainer(): azdata.FlexContainer { private async createSearchAndRefreshContainer(): Promise<azdata.FlexContainer> {
this._searchBox = this._view.modelBuilder.inputBox().withProps({ this._searchBox = this._view.modelBuilder.inputBox()
stopEnterPropagation: true, .withProps({
placeHolder: loc.SEARCH_FOR_MIGRATIONS, stopEnterPropagation: true,
width: '360px' placeHolder: loc.SEARCH_FOR_MIGRATIONS,
}).component(); width: '360px'
}).component();
this._disposables.push(this._searchBox.onTextChanged(async (value) => { this._disposables.push(
await this.populateMigrationTable(); this._searchBox.onTextChanged(
})); async (value) => await this.populateMigrationTable()));
this._refresh = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.refresh,
iconHeight: '16px',
iconWidth: '20px',
height: '30px',
label: loc.REFRESH_BUTTON_LABEL,
}).component();
this._refresh = this._view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.refresh,
iconHeight: '16px',
iconWidth: '20px',
label: loc.REFRESH_BUTTON_LABEL,
}).component();
this._disposables.push( this._disposables.push(
this._refresh.onDidClick( this._refresh.onDidClick(
async (e) => { await this.refreshTable(); })); async (e) => await this.refreshTable()));
const flexContainer = this._view.modelBuilder.flexContainer().withProps({ this._statusDropdown = this._view.modelBuilder.dropDown()
width: 900, .withProps({
CSSStyles: { ariaLabel: loc.MIGRATION_STATUS_FILTER,
'justify-content': 'left' values: this._model.statusDropdownValues,
}, width: '220px'
}).component(); }).component();
this._disposables.push(
flexContainer.addItem(this._searchBox, { this._statusDropdown.onValueChanged(
flex: '0' async (value) => await this.populateMigrationTable()));
});
this._statusDropdown = this._view.modelBuilder.dropDown().withProps({
ariaLabel: loc.MIGRATION_STATUS_FILTER,
values: this._model.statusDropdownValues,
width: '220px'
}).component();
this._disposables.push(this._statusDropdown.onValueChanged(async (value) => {
await this.populateMigrationTable();
}));
if (this._filter) { if (this._filter) {
this._statusDropdown.value = (<azdata.CategoryValue[]>this._statusDropdown.values).find((value) => { this._statusDropdown.value =
return value.name === this._filter; (<azdata.CategoryValue[]>this._statusDropdown.values)
}); .find(value => value.name === this._filter);
} }
flexContainer.addItem(this._statusDropdown, { this._refreshLoader = this._view.modelBuilder.loadingComponent()
flex: '0', .withProps({ loading: false })
CSSStyles: { .component();
'margin-left': '20px'
}
});
flexContainer.addItem(this._refresh, { const searchLabel = this._view.modelBuilder.text()
flex: '0', .withProps({
CSSStyles: { value: 'Status',
'margin-left': '20px' CSSStyles: {
} 'font-size': '13px',
}); 'font-weight': '600',
'margin': '3px 0 0 0',
},
}).component();
this._refreshLoader = this._view.modelBuilder.loadingComponent().withProps({ const serviceContextLabel = await getSelectedServiceStatus();
loading: false, this._serviceContextButton = this._view.modelBuilder.button()
height: '55px' .withProps({
}).component(); iconPath: IconPathHelper.sqlMigrationService,
iconHeight: 22,
iconWidth: 22,
label: serviceContextLabel,
title: serviceContextLabel,
description: loc.MIGRATION_SERVICE_DESCRIPTION,
buttonType: azdata.ButtonType.Informational,
width: 270,
}).component();
flexContainer.addItem(this._refreshLoader, { const onDialogClosed = async (): Promise<void> => {
flex: '0 0 auto', const label = await getSelectedServiceStatus();
CSSStyles: { this._serviceContextButton.label = label;
'margin-left': '20px' this._serviceContextButton.title = label;
} await this.refreshTable();
}); };
this.setAutoRefresh(refreshFrequency);
const container = this._view.modelBuilder.flexContainer().withProps({ this._disposables.push(
width: 1000 this._serviceContextButton.onDidClick(
}).component(); async () => {
container.addItem(flexContainer, { const dialog = new SelectMigrationServiceDialog(onDialogClosed);
flex: '0 0 auto', await dialog.initialize();
CSSStyles: { }));
'width': '980px'
} const flexContainer = this._view.modelBuilder.flexContainer()
}); .withProps({
width: '100%',
CSSStyles: {
'justify-content': 'left',
'align-items': 'center',
'padding': '0px',
'display': 'flex',
'flex-direction': 'row',
},
}).component();
flexContainer.addItem(this._searchBox, { flex: '0' });
flexContainer.addItem(this._serviceContextButton, { flex: '0', CSSStyles: { 'margin-left': '20px' } });
flexContainer.addItem(searchLabel, { flex: '0', CSSStyles: { 'margin-left': '20px' } });
flexContainer.addItem(this._statusDropdown, { flex: '0', CSSStyles: { 'margin-left': '5px' } });
flexContainer.addItem(this._refresh, { flex: '0', CSSStyles: { 'margin-left': '20px' } });
flexContainer.addItem(this._refreshLoader, { flex: '0 0 auto', CSSStyles: { 'margin-left': '20px' } });
const container = this._view.modelBuilder.flexContainer()
.withProps({ width: 1245 })
.component();
container.addItem(flexContainer, { flex: '0 0 auto', });
return container; return container;
} }
private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void {
const classVariable = this;
clearInterval(this._autoRefreshHandle);
if (interval !== -1) {
this._autoRefreshHandle = setInterval(async function () { await classVariable.refreshTable(); }, interval);
}
}
private registerCommands(): void { private registerCommands(): void {
this._disposables.push(vscode.commands.registerCommand( this._disposables.push(vscode.commands.registerCommand(
MenuCommands.Cutover, MenuCommands.Cutover,
async (migrationId: string) => { async (migrationId: string) => {
try { try {
clearDialogMessage(this._dialogObject); clearDialogMessage(this._dialogObject);
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId); const migration = this._model._migrations.find(
if (this.canCutoverMigration(migration?.migrationContext.properties.migrationStatus)) { migration => migration.id === migrationId);
const cutoverDialogModel = new MigrationCutoverDialogModel(migration!);
if (this.canCutoverMigration(migration?.properties.migrationStatus)) {
const cutoverDialogModel = new MigrationCutoverDialogModel(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
await cutoverDialogModel.fetchStatus(); await cutoverDialogModel.fetchStatus();
const dialog = new ConfirmCutoverDialog(cutoverDialogModel); const dialog = new ConfirmCutoverDialog(cutoverDialogModel);
await dialog.initialize(); await dialog.initialize();
@@ -224,8 +237,12 @@ export class MigrationStatusDialog {
MenuCommands.ViewDatabase, MenuCommands.ViewDatabase,
async (migrationId: string) => { async (migrationId: string) => {
try { try {
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId); const migration = this._model._migrations.find(migration => migration.id === migrationId);
const dialog = new MigrationCutoverDialog(this._context, migration!); const dialog = new MigrationCutoverDialog(
this._context,
await MigrationLocalStorage.getMigrationServiceContext(),
migration!,
this._onClosedCallback);
await dialog.initialize(); await dialog.initialize();
} catch (e) { } catch (e) {
console.log(e); console.log(e);
@@ -236,8 +253,8 @@ export class MigrationStatusDialog {
MenuCommands.ViewTarget, MenuCommands.ViewTarget,
async (migrationId: string) => { async (migrationId: string) => {
try { try {
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId); const migration = this._model._migrations.find(migration => migration.id === migrationId);
const url = 'https://portal.azure.com/#resource/' + migration!.targetManagedInstance.id; const url = 'https://portal.azure.com/#resource/' + migration!.properties.scope;
await vscode.env.openExternal(vscode.Uri.parse(url)); await vscode.env.openExternal(vscode.Uri.parse(url));
} catch (e) { } catch (e) {
console.log(e); console.log(e);
@@ -248,8 +265,10 @@ export class MigrationStatusDialog {
MenuCommands.ViewService, MenuCommands.ViewService,
async (migrationId: string) => { async (migrationId: string) => {
try { try {
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId); const migration = this._model._migrations.find(migration => migration.id === migrationId);
const dialog = new SqlMigrationServiceDetailsDialog(migration!); const dialog = new SqlMigrationServiceDetailsDialog(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
await dialog.initialize(); await dialog.initialize();
} catch (e) { } catch (e) {
console.log(e); console.log(e);
@@ -261,17 +280,12 @@ export class MigrationStatusDialog {
async (migrationId: string) => { async (migrationId: string) => {
try { try {
clearDialogMessage(this._dialogObject); clearDialogMessage(this._dialogObject);
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId); const migration = this._model._migrations.find(migration => migration.id === migrationId);
const cutoverDialogModel = new MigrationCutoverDialogModel(migration!); const cutoverDialogModel = new MigrationCutoverDialogModel(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
await cutoverDialogModel.fetchStatus(); await cutoverDialogModel.fetchStatus();
if (cutoverDialogModel.migrationOpStatus) { await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migrationStatus, undefined, 2));
await vscode.env.clipboard.writeText(JSON.stringify({
'async-operation-details': cutoverDialogModel.migrationOpStatus,
'details': cutoverDialogModel.migrationStatus
}, undefined, 2));
} else {
await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migrationStatus, undefined, 2));
}
await vscode.window.showInformationMessage(loc.DETAILS_COPIED); await vscode.window.showInformationMessage(loc.DETAILS_COPIED);
} catch (e) { } catch (e) {
@@ -285,11 +299,13 @@ export class MigrationStatusDialog {
async (migrationId: string) => { async (migrationId: string) => {
try { try {
clearDialogMessage(this._dialogObject); clearDialogMessage(this._dialogObject);
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId); const migration = this._model._migrations.find(migration => migration.id === migrationId);
if (this.canCancelMigration(migration?.migrationContext.properties.migrationStatus)) { if (this.canCancelMigration(migration?.properties.migrationStatus)) {
void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => { void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => {
if (v === loc.YES) { if (v === loc.YES) {
const cutoverDialogModel = new MigrationCutoverDialogModel(migration!); const cutoverDialogModel = new MigrationCutoverDialogModel(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
await cutoverDialogModel.fetchStatus(); await cutoverDialogModel.fetchStatus();
await cutoverDialogModel.cancelMigration(); await cutoverDialogModel.cancelMigration();
@@ -312,9 +328,13 @@ export class MigrationStatusDialog {
async (migrationId: string) => { async (migrationId: string) => {
try { try {
clearDialogMessage(this._dialogObject); clearDialogMessage(this._dialogObject);
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId); const migration = this._model._migrations.find(migration => migration.id === migrationId);
if (canRetryMigration(migration?.migrationContext.properties.migrationStatus)) { if (canRetryMigration(migration?.properties.migrationStatus)) {
let retryMigrationDialog = new RetryMigrationDialog(this._context, migration!); let retryMigrationDialog = new RetryMigrationDialog(
this._context,
await MigrationLocalStorage.getMigrationServiceContext(),
migration!,
this._onClosedCallback);
await retryMigrationDialog.openDialog(); await retryMigrationDialog.openDialog();
} }
else { else {
@@ -329,75 +349,43 @@ export class MigrationStatusDialog {
private async populateMigrationTable(): Promise<void> { private async populateMigrationTable(): Promise<void> {
try { try {
const migrations = filterMigrations( this._filteredMigrations = filterMigrations(
this._model._migrations, this._model._migrations,
(<azdata.CategoryValue>this._statusDropdown.value).name, (<azdata.CategoryValue>this._statusDropdown.value).name,
this._searchBox.value!); this._searchBox.value!);
migrations.sort((m1, m2) => { this._filteredMigrations.sort((m1, m2) => {
return new Date(m1.migrationContext.properties?.startedOn) > new Date(m2.migrationContext.properties?.startedOn) ? -1 : 1; return new Date(m1.properties?.startedOn) > new Date(m2.properties?.startedOn) ? -1 : 1;
}); });
const data: azdata.DeclarativeTableCellValue[][] = migrations.map((migration, index) => { const data: any[] = this._filteredMigrations.map((migration, index) => {
return [ return [
{ value: this._getDatabaserHyperLink(migration) }, <azdata.HyperlinkColumnCellValue>{
{ value: this._getMigrationStatus(migration) }, icon: IconPathHelper.sqlDatabaseLogo,
{ value: getMigrationMode(migration) }, title: migration.properties.sourceDatabaseName ?? '-',
{ value: getMigrationTargetType(migration) }, }, // database
{ value: migration.targetManagedInstance.name }, <azdata.HyperlinkColumnCellValue>{
{ value: migration.controller.name }, icon: getMigrationStatusImage(migration.properties.migrationStatus),
{ title: this._getMigrationStatus(migration),
value: this._getMigrationDuration( }, // statue
migration.migrationContext.properties.startedOn, getMigrationMode(migration), // mode
migration.migrationContext.properties.endedOn) getMigrationTargetType(migration), // targetType
}, getResourceName(migration.id), // targetName
{ value: this._getMigrationTime(migration.migrationContext.properties.startedOn) }, getResourceName(migration.properties.migrationService), // migrationService
{ value: this._getMigrationTime(migration.migrationContext.properties.endedOn) }, this._getMigrationDuration(
{ migration.properties.startedOn,
value: { migration.properties.endedOn), // duration
commands: this._getMenuCommands(migration), this._getMigrationTime(migration.properties.startedOn), // startTime
context: migration.migrationContext.id this._getMigrationTime(migration.properties.endedOn), // endTime
},
}
]; ];
}); });
await this._statusTable.setDataValues(data); await this._statusTable.updateProperty('data', data);
} catch (e) { } catch (e) {
console.log(e); logError(TelemetryViews.MigrationStatusDialog, 'Error populating migrations list page', e);
} }
} }
private _getDatabaserHyperLink(migration: MigrationContext): azdata.FlexContainer {
const imageControl = this._view.modelBuilder.image()
.withProps({
iconPath: IconPathHelper.sqlDatabaseLogo,
iconHeight: statusImageSize,
iconWidth: statusImageSize,
height: statusImageSize,
width: statusImageSize,
CSSStyles: imageCellStyles
})
.component();
const databaseHyperLink = this._view.modelBuilder
.hyperlink()
.withProps({
label: migration.migrationContext.properties.sourceDatabaseName,
url: '',
CSSStyles: statusCellStyles
}).component();
this._disposables.push(databaseHyperLink.onDidClick(
async (e) => await (new MigrationCutoverDialog(this._context, migration)).initialize()));
return this._view.modelBuilder
.flexContainer()
.withItems([imageControl, databaseHyperLink])
.withProps({ CSSStyles: statusCellStyles, display: 'inline-flex' })
.component();
}
private _getMigrationTime(migrationTime: string): string { private _getMigrationTime(migrationTime: string): string {
return migrationTime return migrationTime
? new Date(migrationTime).toLocaleString() ? new Date(migrationTime).toLocaleString()
@@ -420,39 +408,11 @@ export class MigrationStatusDialog {
return '---'; return '---';
} }
private _getMenuCommands(migration: MigrationContext): string[] { private _getMigrationStatus(migration: DatabaseMigration): string {
const menuCommands: string[] = []; const properties = migration.properties;
const migrationStatus = migration?.migrationContext?.properties?.migrationStatus;
if (getMigrationMode(migration) === loc.ONLINE &&
this.canCutoverMigration(migrationStatus)) {
menuCommands.push(MenuCommands.Cutover);
}
menuCommands.push(...[
MenuCommands.ViewDatabase,
MenuCommands.ViewTarget,
MenuCommands.ViewService,
MenuCommands.CopyMigration]);
if (this.canCancelMigration(migrationStatus)) {
menuCommands.push(MenuCommands.CancelMigration);
}
if (canRetryMigration(migrationStatus)) {
menuCommands.push(MenuCommands.RetryMigration);
}
return menuCommands;
}
private _getMigrationStatus(migration: MigrationContext): azdata.FlexContainer {
const properties = migration.migrationContext.properties;
const migrationStatus = properties.migrationStatus ?? properties.provisioningState; const migrationStatus = properties.migrationStatus ?? properties.provisioningState;
let warningCount = 0; let warningCount = 0;
if (migration.asyncOperationResult?.error?.message) {
warningCount++;
}
if (properties.migrationFailureError?.message) { if (properties.migrationFailureError?.message) {
warningCount++; warningCount++;
} }
@@ -463,17 +423,16 @@ export class MigrationStatusDialog {
warningCount++; warningCount++;
} }
return this._getStatusControl(migrationStatus, warningCount, migration); return loc.STATUS_VALUE(migrationStatus, warningCount) + (loc.STATUS_WARNING_COUNT(migrationStatus, warningCount) ?? '');
} }
public openCalloutDialog(dialogHeading: string, dialogName?: string, calloutMessageText?: string): void { public openCalloutDialog(dialogHeading: string, dialogName?: string, calloutMessageText?: string): void {
const dialog = azdata.window.createModelViewDialog(dialogHeading, dialogName, 288, 'callout', 'left', true, false, const dialog = azdata.window.createModelViewDialog(dialogHeading, dialogName, 288, 'callout', 'left', true, false, {
{ xPos: 0,
xPos: 0, yPos: 0,
yPos: 0, width: 20,
width: 20, height: 20
height: 20 });
});
const tab: azdata.window.DialogTab = azdata.window.createTab(''); const tab: azdata.window.DialogTab = azdata.window.createTab('');
tab.registerContent(async view => { tab.registerContent(async view => {
const warningContentContainer = view.modelBuilder.divContainer().component(); const warningContentContainer = view.modelBuilder.divContainer().component();
@@ -499,73 +458,6 @@ export class MigrationStatusDialog {
azdata.window.openDialog(dialog); azdata.window.openDialog(dialog);
} }
private _getStatusControl(status: string, count: number, migration: MigrationContext): azdata.DivContainer {
const control = this._view.modelBuilder
.divContainer()
.withItems([
// migration status icon
this._view.modelBuilder.image()
.withProps({
iconPath: getMigrationStatusImage(status),
iconHeight: statusImageSize,
iconWidth: statusImageSize,
height: statusImageSize,
width: statusImageSize,
CSSStyles: imageCellStyles
})
.component(),
// migration status text
this._view.modelBuilder.text().withProps({
value: loc.STATUS_VALUE(status, count),
height: statusImageSize,
CSSStyles: statusCellStyles,
}).component()
])
.withProps({ CSSStyles: statusCellStyles, display: 'inline-flex' })
.component();
if (count > 0) {
const migrationWarningImage = this._view.modelBuilder.image()
.withProps({
iconPath: this._statusInfoMap(status),
iconHeight: statusImageSize,
iconWidth: statusImageSize,
height: statusImageSize,
width: statusImageSize,
CSSStyles: imageCellStyles
}).component();
const migrationWarningCount = this._view.modelBuilder.hyperlink()
.withProps({
label: loc.STATUS_WARNING_COUNT(status, count) ?? '',
ariaLabel: loc.ERROR,
url: '',
height: statusImageSize,
CSSStyles: statusCellStyles,
}).component();
control.addItems([
migrationWarningImage,
migrationWarningCount
]);
this._disposables.push(migrationWarningCount.onDidClick(async () => {
const cutoverDialogModel = new MigrationCutoverDialogModel(migration!);
const errors = await cutoverDialogModel.fetchErrors();
this.openCalloutDialog(
status === MigrationStatus.InProgress
|| status === MigrationStatus.Completing
? loc.WARNING
: loc.ERROR,
'input-table-row-dialog',
errors
);
}));
}
return control;
}
private async refreshTable(): Promise<void> { private async refreshTable(): Promise<void> {
if (this.isRefreshing) { if (this.isRefreshing) {
return; return;
@@ -575,8 +467,7 @@ export class MigrationStatusDialog {
try { try {
clearDialogMessage(this._dialogObject); clearDialogMessage(this._dialogObject);
this._refreshLoader.loading = true; this._refreshLoader.loading = true;
const currentConnection = await azdata.connection.getCurrentConnection(); this._model._migrations = await getCurrentMigrations();
this._model._migrations = await MigrationLocalStorage.getMigrationsBySourceConnections(currentConnection, true);
await this.populateMigrationTable(); await this.populateMigrationTable();
} catch (e) { } catch (e) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_STATUS_REFRESH_ERROR, e); displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_STATUS_REFRESH_ERROR, e);
@@ -587,115 +478,111 @@ export class MigrationStatusDialog {
} }
} }
private createStatusTable(): azdata.DeclarativeTableComponent { private createStatusTable(): azdata.TableComponent {
const rowCssStyle: azdata.CssStyles = { const headerCssStyles = undefined;
'border': 'none', const rowCssStyles = undefined;
'text-align': 'left',
'border-bottom': '1px solid',
};
const headerCssStyles: azdata.CssStyles = { this._statusTable = this._view.modelBuilder.table().withProps({
'border': 'none',
'text-align': 'left',
'border-bottom': '1px solid',
'font-weight': 'bold',
'padding-left': '0px',
'padding-right': '0px'
};
this._statusTable = this._view.modelBuilder.declarativeTable().withProps({
ariaLabel: loc.MIGRATION_STATUS, ariaLabel: loc.MIGRATION_STATUS,
data: [],
forceFitColumns: azdata.ColumnSizingMode.ForceFit,
height: '600px',
width: '1095px',
display: 'grid',
columns: [ columns: [
{ <azdata.HyperlinkColumn>{
displayName: loc.DATABASE, cssClass: rowCssStyles,
valueType: azdata.DeclarativeDataType.component, headerCssClass: headerCssStyles,
width: '90px', name: loc.DATABASE,
isReadOnly: true, value: 'database',
rowCssStyles: rowCssStyle, width: 190,
headerCssStyles: headerCssStyles type: azdata.ColumnType.hyperlink,
icon: IconPathHelper.sqlDatabaseLogo,
showText: true,
},
<azdata.HyperlinkColumn>{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.STATUS_COLUMN,
value: 'status',
width: 120,
type: azdata.ColumnType.hyperlink,
}, },
{ {
displayName: loc.MIGRATION_STATUS, cssClass: rowCssStyles,
valueType: azdata.DeclarativeDataType.component, headerCssClass: headerCssStyles,
width: '170px', name: loc.MIGRATION_MODE,
isReadOnly: true, value: 'mode',
rowCssStyles: rowCssStyle, width: 85,
headerCssStyles: headerCssStyles type: azdata.ColumnType.text,
}, },
{ {
displayName: loc.MIGRATION_MODE, cssClass: rowCssStyles,
valueType: azdata.DeclarativeDataType.string, headerCssClass: headerCssStyles,
width: '90px', name: loc.AZURE_SQL_TARGET,
isReadOnly: true, value: 'targetType',
rowCssStyles: rowCssStyle, width: 120,
headerCssStyles: headerCssStyles type: azdata.ColumnType.text,
}, },
{ {
displayName: loc.AZURE_SQL_TARGET, cssClass: rowCssStyles,
valueType: azdata.DeclarativeDataType.string, headerCssClass: headerCssStyles,
width: '130px', name: loc.TARGET_AZURE_SQL_INSTANCE_NAME,
isReadOnly: true, value: 'targetName',
rowCssStyles: rowCssStyle, width: 125,
headerCssStyles: headerCssStyles type: azdata.ColumnType.text,
}, },
{ {
displayName: loc.TARGET_AZURE_SQL_INSTANCE_NAME, cssClass: rowCssStyles,
valueType: azdata.DeclarativeDataType.string, headerCssClass: headerCssStyles,
width: '130px', name: loc.DATABASE_MIGRATION_SERVICE,
isReadOnly: true, value: 'migrationService',
rowCssStyles: rowCssStyle, width: 140,
headerCssStyles: headerCssStyles type: azdata.ColumnType.text,
}, },
{ {
displayName: loc.DATABASE_MIGRATION_SERVICE, cssClass: rowCssStyles,
valueType: azdata.DeclarativeDataType.string, headerCssClass: headerCssStyles,
width: '150px', name: loc.DURATION,
isReadOnly: true, value: 'duration',
rowCssStyles: rowCssStyle, width: 50,
headerCssStyles: headerCssStyles type: azdata.ColumnType.text,
}, },
{ {
displayName: loc.DURATION, cssClass: rowCssStyles,
valueType: azdata.DeclarativeDataType.string, headerCssClass: headerCssStyles,
width: '55px', name: loc.START_TIME,
isReadOnly: true, value: 'startTime',
rowCssStyles: rowCssStyle, width: 115,
headerCssStyles: headerCssStyles type: azdata.ColumnType.text,
}, },
{ {
displayName: loc.START_TIME, cssClass: rowCssStyles,
valueType: azdata.DeclarativeDataType.string, headerCssClass: headerCssStyles,
width: '140px', name: loc.FINISH_TIME,
isReadOnly: true, value: 'finishTime',
rowCssStyles: rowCssStyle, width: 115,
headerCssStyles: headerCssStyles type: azdata.ColumnType.text,
}, },
{
displayName: loc.FINISH_TIME,
valueType: azdata.DeclarativeDataType.string,
width: '140px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: '',
valueType: azdata.DeclarativeDataType.menu,
width: '20px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles,
}
] ]
}).component(); }).component();
this._disposables.push(this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => {
const buttonState = <azdata.ICellActionEventArgs>rowState;
switch (buttonState?.column) {
case 0:
case 1:
const migration = this._filteredMigrations[rowState.row];
const dialog = new MigrationCutoverDialog(
this._context,
await MigrationLocalStorage.getMigrationServiceContext(),
migration,
this._onClosedCallback);
await dialog.initialize();
break;
}
}));
return this._statusTable; return this._statusTable;
} }
private _statusInfoMap(status: string): azdata.IconPath {
return status === MigrationStatus.InProgress
|| status === MigrationStatus.Creating
|| status === MigrationStatus.Completing
? IconPathHelper.warning
: IconPathHelper.error;
}
} }

View File

@@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import { DatabaseMigration } from '../../api/azure';
import * as loc from '../../constants/strings'; import * as loc from '../../constants/strings';
import { MigrationContext } from '../../models/migrationLocalStorage';
export class MigrationStatusDialogModel { export class MigrationStatusDialogModel {
public statusDropdownValues: azdata.CategoryValue[] = [ public statusDropdownValues: azdata.CategoryValue[] = [
@@ -27,7 +27,7 @@ export class MigrationStatusDialogModel {
} }
]; ];
constructor(public _migrations: MigrationContext[]) { constructor(public _migrations: DatabaseMigration[]) {
} }
} }

View File

@@ -7,26 +7,26 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as mssql from 'mssql'; import * as mssql from 'mssql';
import { azureResource } from 'azureResource'; import { azureResource } from 'azureResource';
import { getLocations, getResourceGroupFromId, getBlobContainerId, getFullResourceGroupFromId, getResourceName } from '../../api/azure'; import { getLocations, getResourceGroupFromId, getBlobContainerId, getFullResourceGroupFromId, getResourceName, DatabaseMigration, getMigrationTargetInstance } from '../../api/azure';
import { MigrationMode, MigrationStateModel, NetworkContainerType, SavedInfo } from '../../models/stateMachine'; import { MigrationMode, MigrationStateModel, NetworkContainerType, SavedInfo } from '../../models/stateMachine';
import { MigrationContext } from '../../models/migrationLocalStorage'; import { MigrationServiceContext } from '../../models/migrationLocalStorage';
import { WizardController } from '../../wizard/wizardController'; import { WizardController } from '../../wizard/wizardController';
import { getMigrationModeEnum, getMigrationTargetTypeEnum } from '../../constants/helper'; import { getMigrationModeEnum, getMigrationTargetTypeEnum } from '../../constants/helper';
import * as constants from '../../constants/strings'; import * as constants from '../../constants/strings';
export class RetryMigrationDialog { export class RetryMigrationDialog {
private _context: vscode.ExtensionContext;
private _migration: MigrationContext;
constructor(context: vscode.ExtensionContext, migration: MigrationContext) { constructor(
this._context = context; private readonly _context: vscode.ExtensionContext,
this._migration = migration; private readonly _serviceContext: MigrationServiceContext,
private readonly _migration: DatabaseMigration,
private readonly _onClosedCallback: () => Promise<void>) {
} }
private createMigrationStateModel(migration: MigrationContext, connectionId: string, serverName: string, api: mssql.IExtension, location: azureResource.AzureLocation): MigrationStateModel { private async createMigrationStateModel(serviceContext: MigrationServiceContext, migration: DatabaseMigration, connectionId: string, serverName: string, api: mssql.IExtension, location: azureResource.AzureLocation): Promise<MigrationStateModel> {
let stateModel = new MigrationStateModel(this._context, connectionId, api.sqlMigration); let stateModel = new MigrationStateModel(this._context, connectionId, api.sqlMigration);
const sourceDatabaseName = migration.migrationContext.properties.sourceDatabaseName; const sourceDatabaseName = migration.properties.sourceDatabaseName;
let savedInfo: SavedInfo; let savedInfo: SavedInfo;
savedInfo = { savedInfo = {
closedPage: 0, closedPage: 0,
@@ -41,53 +41,56 @@ export class RetryMigrationDialog {
migrationTargetType: getMigrationTargetTypeEnum(migration)!, migrationTargetType: getMigrationTargetTypeEnum(migration)!,
// TargetSelection // TargetSelection
azureAccount: migration.azureAccount, azureAccount: serviceContext.azureAccount!,
azureTenant: migration.azureAccount.properties.tenants[0], azureTenant: serviceContext.azureAccount!.properties.tenants[0]!,
subscription: migration.subscription, subscription: serviceContext.subscription!,
location: location, location: location,
resourceGroup: { resourceGroup: {
id: getFullResourceGroupFromId(migration.targetManagedInstance.id), id: getFullResourceGroupFromId(migration.id),
name: getResourceGroupFromId(migration.targetManagedInstance.id), name: getResourceGroupFromId(migration.id),
subscription: migration.subscription subscription: serviceContext.subscription!,
}, },
targetServerInstance: migration.targetManagedInstance, targetServerInstance: await getMigrationTargetInstance(
serviceContext.azureAccount!,
serviceContext.subscription!,
migration),
// MigrationMode // MigrationMode
migrationMode: getMigrationModeEnum(migration), migrationMode: getMigrationModeEnum(migration),
// DatabaseBackup // DatabaseBackup
targetDatabaseNames: [migration.migrationContext.name], targetDatabaseNames: [migration.name],
networkContainerType: null, networkContainerType: null,
networkShares: [], networkShares: [],
blobs: [], blobs: [],
// Integration Runtime // Integration Runtime
sqlMigrationService: migration.controller, sqlMigrationService: serviceContext.migrationService,
}; };
const getStorageAccountResourceGroup = (storageAccountResourceId: string) => { const getStorageAccountResourceGroup = (storageAccountResourceId: string): azureResource.AzureResourceResourceGroup => {
return { return {
id: getFullResourceGroupFromId(storageAccountResourceId!), id: getFullResourceGroupFromId(storageAccountResourceId!),
name: getResourceGroupFromId(storageAccountResourceId!), name: getResourceGroupFromId(storageAccountResourceId!),
subscription: migration.subscription subscription: this._serviceContext.subscription!
}; };
}; };
const getStorageAccount = (storageAccountResourceId: string) => { const getStorageAccount = (storageAccountResourceId: string): azureResource.AzureGraphResource => {
const storageAccountName = getResourceName(storageAccountResourceId); const storageAccountName = getResourceName(storageAccountResourceId);
return { return {
type: 'microsoft.storage/storageaccounts', type: 'microsoft.storage/storageaccounts',
id: storageAccountResourceId!, id: storageAccountResourceId!,
tenantId: savedInfo.azureTenant?.id!, tenantId: savedInfo.azureTenant?.id!,
subscriptionId: migration.subscription.id, subscriptionId: this._serviceContext.subscription?.id!,
name: storageAccountName, name: storageAccountName,
location: savedInfo.location!.name, location: savedInfo.location!.name,
}; };
}; };
const sourceLocation = migration.migrationContext.properties.backupConfiguration.sourceLocation; const sourceLocation = migration.properties.backupConfiguration?.sourceLocation;
if (sourceLocation?.fileShare) { if (sourceLocation?.fileShare) {
savedInfo.networkContainerType = NetworkContainerType.NETWORK_SHARE; savedInfo.networkContainerType = NetworkContainerType.NETWORK_SHARE;
const storageAccountResourceId = migration.migrationContext.properties.backupConfiguration.targetLocation?.storageAccountResourceId!; const storageAccountResourceId = migration.properties.backupConfiguration?.targetLocation?.storageAccountResourceId!;
savedInfo.networkShares = [ savedInfo.networkShares = [
{ {
password: '', password: '',
@@ -106,9 +109,9 @@ export class RetryMigrationDialog {
blobContainer: { blobContainer: {
id: getBlobContainerId(getFullResourceGroupFromId(storageAccountResourceId!), getResourceName(storageAccountResourceId!), sourceLocation?.azureBlob.blobContainerName), id: getBlobContainerId(getFullResourceGroupFromId(storageAccountResourceId!), getResourceName(storageAccountResourceId!), sourceLocation?.azureBlob.blobContainerName),
name: sourceLocation?.azureBlob.blobContainerName, name: sourceLocation?.azureBlob.blobContainerName,
subscription: migration.subscription subscription: this._serviceContext.subscription!
}, },
lastBackupFile: getMigrationModeEnum(migration) === MigrationMode.OFFLINE ? migration.migrationContext.properties.offlineConfiguration.lastBackupName! : undefined, lastBackupFile: getMigrationModeEnum(migration) === MigrationMode.OFFLINE ? migration.properties.offlineConfiguration?.lastBackupName! : undefined,
storageAccount: getStorageAccount(storageAccountResourceId!), storageAccount: getStorageAccount(storageAccountResourceId!),
resourceGroup: getStorageAccountResourceGroup(storageAccountResourceId!), resourceGroup: getStorageAccountResourceGroup(storageAccountResourceId!),
storageKey: '' storageKey: ''
@@ -123,10 +126,18 @@ export class RetryMigrationDialog {
} }
public async openDialog(dialogName?: string) { public async openDialog(dialogName?: string) {
const locations = await getLocations(this._migration.azureAccount, this._migration.subscription); const locations = await getLocations(
this._serviceContext.azureAccount!,
this._serviceContext.subscription!);
const targetInstance = await getMigrationTargetInstance(
this._serviceContext.azureAccount!,
this._serviceContext.subscription!,
this._migration);
let location: azureResource.AzureLocation; let location: azureResource.AzureLocation;
locations.forEach(azureLocation => { locations.forEach(azureLocation => {
if (azureLocation.name === this._migration.targetManagedInstance.location) { if (azureLocation.name === targetInstance.location) {
location = azureLocation; location = azureLocation;
} }
}); });
@@ -146,10 +157,13 @@ export class RetryMigrationDialog {
} }
const api = (await vscode.extensions.getExtension(mssql.extension.name)?.activate()) as mssql.IExtension; const api = (await vscode.extensions.getExtension(mssql.extension.name)?.activate()) as mssql.IExtension;
const stateModel = this.createMigrationStateModel(this._migration, connectionId, serverName, api, location!); const stateModel = await this.createMigrationStateModel(this._serviceContext, this._migration, connectionId, serverName, api, location!);
if (stateModel.loadSavedInfo()) { if (await stateModel.loadSavedInfo()) {
const wizardController = new WizardController(this._context, stateModel); const wizardController = new WizardController(
this._context,
stateModel,
this._onClosedCallback);
await wizardController.openWizard(stateModel.sourceConnectionId); await wizardController.openWizard(stateModel.sourceConnectionId);
} else { } else {
void vscode.window.showInformationMessage(constants.MIGRATION_CANNOT_RETRY); void vscode.window.showInformationMessage(constants.MIGRATION_CANNOT_RETRY);

View File

@@ -0,0 +1,597 @@
/*---------------------------------------------------------------------------------------------
* 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 azurecore from 'azurecore';
import { MigrationLocalStorage, MigrationServiceContext } from '../../models/migrationLocalStorage';
import { azureResource } from 'azureResource';
import * as styles from '../../constants/styles';
import * as constants from '../../constants/strings';
import { findDropDownItemIndex, selectDefaultDropdownValue, deepClone } from '../../api/utils';
import { getFullResourceGroupFromId, getLocations, getSqlMigrationServices, getSubscriptions, SqlMigrationService } from '../../api/azure';
import { logError, TelemetryViews } from '../../telemtery';
const CONTROL_MARGIN = '20px';
const INPUT_COMPONENT_WIDTH = '100%';
const STYLE_HIDE = { 'display': 'none' };
const STYLE_ShOW = { 'display': 'inline' };
export const BODY_CSS = {
'font-size': '13px',
'line-height': '18px',
'margin': '4px 0',
};
const LABEL_CSS = {
...styles.LABEL_CSS,
'margin': '0 0 0 0',
'font-weight': '600',
};
const DROPDOWN_CSS = {
'margin': '-1em 0 0 0',
};
const TENANT_DROPDOWN_CSS = {
'margin': '1em 0 0 0',
};
export class SelectMigrationServiceDialog {
private _dialog: azdata.window.Dialog;
private _view!: azdata.ModelView;
private _disposables: vscode.Disposable[] = [];
private _serviceContext!: MigrationServiceContext;
private _azureAccounts!: azdata.Account[];
private _accountTenants!: azurecore.Tenant[];
private _subscriptions!: azureResource.AzureResourceSubscription[];
private _locations!: azureResource.AzureLocation[];
private _resourceGroups!: azureResource.AzureResourceResourceGroup[];
private _sqlMigrationServices!: SqlMigrationService[];
private _azureAccountsDropdown!: azdata.DropDownComponent;
private _accountTenantDropdown!: azdata.DropDownComponent;
private _accountTenantFlexContainer!: azdata.FlexContainer;
private _azureSubscriptionDropdown!: azdata.DropDownComponent;
private _azureLocationDropdown!: azdata.DropDownComponent;
private _azureResourceGroupDropdown!: azdata.DropDownComponent;
private _azureServiceDropdownLabel!: azdata.TextComponent;
private _azureServiceDropdown!: azdata.DropDownComponent;
private _deleteButton!: azdata.window.Button;
constructor(
private readonly _onClosedCallback: () => Promise<void>) {
this._dialog = azdata.window.createModelViewDialog(
constants.MIGRATION_SERVICE_SELECT_TITLE,
'SelectMigraitonServiceDialog',
460,
'normal');
}
async initialize(): Promise<void> {
this._serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
await this._dialog.registerContent(async (view: azdata.ModelView) => {
this._disposables.push(
view.onClosed(e => {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
}));
await this.registerContent(view);
});
this._dialog.okButton.label = constants.MIGRATION_SERVICE_SELECT_APPLY_LABEL;
this._dialog.okButton.position = 'left';
this._dialog.cancelButton.position = 'right';
this._deleteButton = azdata.window.createButton(
constants.MIGRATION_SERVICE_CLEAR,
'right');
this._disposables.push(
this._deleteButton.onClick(async (value) => {
await MigrationLocalStorage.saveMigrationServiceContext({});
await this._onClosedCallback();
azdata.window.closeDialog(this._dialog);
}));
this._dialog.customButtons = [this._deleteButton];
azdata.window.openDialog(this._dialog);
}
protected async registerContent(view: azdata.ModelView): Promise<void> {
this._view = view;
const flexContainer = this._view.modelBuilder
.flexContainer()
.withItems([
this._createHeading(),
this._createAzureAccountsDropdown(),
this._createAzureTenantContainer(),
this._createServiceSelectionContainer(),
])
.withLayout({ flexFlow: 'column' })
.withProps({ CSSStyles: { 'padding': CONTROL_MARGIN } })
.component();
await this._view.initializeModel(flexContainer);
await this._populateAzureAccountsDropdown();
}
private _createHeading(): azdata.TextComponent {
return this._view.modelBuilder.text()
.withProps({
value: constants.MIGRATION_SERVICE_SELECT_HEADING,
CSSStyles: { ...styles.BODY_CSS }
}).component();
}
private _createAzureAccountsDropdown(): azdata.FlexContainer {
const azureAccountLabel = this._view.modelBuilder.text()
.withProps({
value: constants.ACCOUNTS_SELECTION_PAGE_TITLE,
requiredIndicator: true,
CSSStyles: { ...LABEL_CSS }
}).component();
this._azureAccountsDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.ACCOUNTS_SELECTION_PAGE_TITLE,
width: INPUT_COMPONENT_WIDTH,
editable: true,
required: true,
fireOnTextChange: true,
placeholder: constants.SELECT_AN_ACCOUNT,
CSSStyles: { ...DROPDOWN_CSS },
}).component();
this._disposables.push(
this._azureAccountsDropdown.onValueChanged(async (value) => {
const selectedIndex = findDropDownItemIndex(this._azureAccountsDropdown, value);
this._serviceContext.azureAccount = (selectedIndex > -1)
? deepClone(this._azureAccounts[selectedIndex])
: undefined!;
await this._populateTentantsDropdown();
}));
const linkAccountButton = this._view.modelBuilder.hyperlink()
.withProps({
label: constants.ACCOUNT_LINK_BUTTON_LABEL,
url: '',
CSSStyles: { ...styles.BODY_CSS },
}).component();
this._disposables.push(
linkAccountButton.onDidClick(async (event) => {
await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount');
await this._populateAzureAccountsDropdown();
}));
return this._view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([
azureAccountLabel,
this._azureAccountsDropdown,
linkAccountButton,
]).component();
}
private _createAzureTenantContainer(): azdata.FlexContainer {
const azureTenantDropdownLabel = this._view.modelBuilder.text()
.withProps({
value: constants.AZURE_TENANT,
CSSStyles: { ...LABEL_CSS, ...TENANT_DROPDOWN_CSS },
}).component();
this._accountTenantDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.AZURE_TENANT,
width: INPUT_COMPONENT_WIDTH,
editable: true,
fireOnTextChange: true,
placeholder: constants.SELECT_A_TENANT,
}).component();
this._disposables.push(
this._accountTenantDropdown.onValueChanged(async value => {
const selectedIndex = findDropDownItemIndex(this._accountTenantDropdown, value);
this._serviceContext.tenant = (selectedIndex > -1)
? deepClone(this._accountTenants[selectedIndex])
: undefined!;
await this._populateSubscriptionDropdown();
}));
this._accountTenantFlexContainer = this._view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([
azureTenantDropdownLabel,
this._accountTenantDropdown,
])
.withProps({ CSSStyles: { ...STYLE_HIDE, } })
.component();
return this._accountTenantFlexContainer;
}
private _createServiceSelectionContainer(): azdata.FlexContainer {
const subscriptionDropdownLabel = this._view.modelBuilder.text()
.withProps({
value: constants.SUBSCRIPTION,
description: constants.TARGET_SUBSCRIPTION_INFO,
requiredIndicator: true,
CSSStyles: { ...LABEL_CSS }
}).component();
this._azureSubscriptionDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.SUBSCRIPTION,
width: INPUT_COMPONENT_WIDTH,
editable: true,
required: true,
fireOnTextChange: true,
placeholder: constants.SELECT_A_SUBSCRIPTION,
CSSStyles: { ...DROPDOWN_CSS },
}).component();
this._disposables.push(
this._azureSubscriptionDropdown.onValueChanged(async (value) => {
const selectedIndex = findDropDownItemIndex(this._azureSubscriptionDropdown, value);
this._serviceContext.subscription = (selectedIndex > -1)
? deepClone(this._subscriptions[selectedIndex])
: undefined!;
await this._populateLocationDropdown();
}));
const azureLocationLabel = this._view.modelBuilder.text()
.withProps({
value: constants.LOCATION,
description: constants.TARGET_LOCATION_INFO,
requiredIndicator: true,
CSSStyles: { ...LABEL_CSS }
}).component();
this._azureLocationDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.LOCATION,
width: INPUT_COMPONENT_WIDTH,
editable: true,
required: true,
fireOnTextChange: true,
placeholder: constants.SELECT_A_LOCATION,
CSSStyles: { ...DROPDOWN_CSS },
}).component();
this._disposables.push(
this._azureLocationDropdown.onValueChanged(async (value) => {
const selectedIndex = findDropDownItemIndex(this._azureLocationDropdown, value);
this._serviceContext.location = (selectedIndex > -1)
? deepClone(this._locations[selectedIndex])
: undefined!;
await this._populateResourceGroupDropdown();
}));
const azureResourceGroupLabel = this._view.modelBuilder.text()
.withProps({
value: constants.RESOURCE_GROUP,
description: constants.TARGET_RESOURCE_GROUP_INFO,
requiredIndicator: true,
CSSStyles: { ...LABEL_CSS }
}).component();
this._azureResourceGroupDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.RESOURCE_GROUP,
width: INPUT_COMPONENT_WIDTH,
editable: true,
required: true,
fireOnTextChange: true,
placeholder: constants.SELECT_A_RESOURCE_GROUP,
CSSStyles: { ...DROPDOWN_CSS },
}).component();
this._disposables.push(
this._azureResourceGroupDropdown.onValueChanged(async (value) => {
const selectedIndex = findDropDownItemIndex(this._azureResourceGroupDropdown, value);
this._serviceContext.resourceGroup = (selectedIndex > -1)
? deepClone(this._resourceGroups[selectedIndex])
: undefined!;
await this._populateMigrationServiceDropdown();
}));
this._azureServiceDropdownLabel = this._view.modelBuilder.text()
.withProps({
value: constants.MIGRATION_SERVICE_SELECT_SERVICE_LABEL,
description: constants.TARGET_RESOURCE_INFO,
requiredIndicator: true,
CSSStyles: { ...LABEL_CSS }
}).component();
this._azureServiceDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.MIGRATION_SERVICE_SELECT_SERVICE_LABEL,
width: INPUT_COMPONENT_WIDTH,
editable: true,
required: true,
fireOnTextChange: true,
placeholder: constants.SELECT_A_SERVICE,
CSSStyles: { ...DROPDOWN_CSS },
}).component();
this._disposables.push(
this._azureServiceDropdown.onValueChanged(async (value) => {
const selectedIndex = findDropDownItemIndex(this._azureServiceDropdown, value, true);
this._serviceContext.migrationService = (selectedIndex > -1)
? deepClone(this._sqlMigrationServices.find(service => service.name === value))
: undefined!;
await this._updateButtonState();
}));
this._disposables.push(
this._dialog.okButton.onClick(async (value) => {
await MigrationLocalStorage.saveMigrationServiceContext(this._serviceContext);
await this._onClosedCallback();
}));
return this._view.modelBuilder.flexContainer()
.withItems([
subscriptionDropdownLabel,
this._azureSubscriptionDropdown,
azureLocationLabel,
this._azureLocationDropdown,
azureResourceGroupLabel,
this._azureResourceGroupDropdown,
this._azureServiceDropdownLabel,
this._azureServiceDropdown,
]).withLayout({ flexFlow: 'column' })
.component();
}
private async _updateButtonState(): Promise<void> {
this._dialog.okButton.enabled = this._serviceContext.migrationService !== undefined;
}
private async _populateAzureAccountsDropdown(): Promise<void> {
try {
this._azureAccountsDropdown.loading = true;
this._azureAccountsDropdown.values = await this._getAccountDropdownValues();
if (this._azureAccountsDropdown.values.length > 0) {
selectDefaultDropdownValue(
this._azureAccountsDropdown,
this._serviceContext.azureAccount?.displayInfo?.userId,
false);
this._azureAccountsDropdown.loading = false;
}
} catch (error) {
logError(TelemetryViews.SelectMigrationServiceDialog, '_populateAzureAccountsDropdown', error);
void vscode.window.showErrorMessage(
constants.SELECT_ACCOUNT_ERROR,
error.message);
} finally {
this._azureAccountsDropdown.loading = false;
}
}
private async _populateTentantsDropdown(): Promise<void> {
try {
this._accountTenantDropdown.loading = true;
this._accountTenantDropdown.values = this._getTenantDropdownValues(
this._serviceContext.azureAccount);
await this._accountTenantFlexContainer.updateCssStyles(
this._accountTenants.length > 1
? STYLE_ShOW
: STYLE_HIDE);
if (this._accountTenantDropdown.values.length > 0) {
selectDefaultDropdownValue(
this._accountTenantDropdown,
this._serviceContext.tenant?.id,
false);
this._accountTenantDropdown.loading = false;
}
} catch (error) {
logError(TelemetryViews.SelectMigrationServiceDialog, '_populateTentantsDropdown', error);
void vscode.window.showErrorMessage(
constants.SELECT_TENANT_ERROR,
error.message);
} finally {
this._accountTenantDropdown.loading = false;
}
}
private async _populateSubscriptionDropdown(): Promise<void> {
try {
this._azureSubscriptionDropdown.loading = true;
this._azureSubscriptionDropdown.values = await this._getSubscriptionDropdownValues(
this._serviceContext.azureAccount);
if (this._azureSubscriptionDropdown.values.length > 0) {
selectDefaultDropdownValue(
this._azureSubscriptionDropdown,
this._serviceContext.subscription?.id,
false);
this._azureSubscriptionDropdown.loading = false;
}
} catch (error) {
logError(TelemetryViews.SelectMigrationServiceDialog, '_populateSubscriptionDropdown', error);
void vscode.window.showErrorMessage(
constants.SELECT_SUBSCRIPTION_ERROR,
error.message);
} finally {
this._azureSubscriptionDropdown.loading = false;
}
}
private async _populateLocationDropdown(): Promise<void> {
try {
this._azureLocationDropdown.loading = true;
this._azureLocationDropdown.values = await this._getAzureLocationDropdownValues(
this._serviceContext.azureAccount,
this._serviceContext.subscription);
if (this._azureLocationDropdown.values.length > 0) {
selectDefaultDropdownValue(
this._azureLocationDropdown,
this._serviceContext.location?.displayName,
true);
this._azureLocationDropdown.loading = false;
}
} catch (error) {
logError(TelemetryViews.SelectMigrationServiceDialog, '_populateLocationDropdown', error);
void vscode.window.showErrorMessage(
constants.SELECT_LOCATION_ERROR,
error.message);
} finally {
this._azureLocationDropdown.loading = false;
}
}
private async _populateResourceGroupDropdown(): Promise<void> {
try {
this._azureResourceGroupDropdown.loading = true;
this._azureResourceGroupDropdown.values = await this._getAzureResourceGroupDropdownValues(
this._serviceContext.location);
if (this._azureResourceGroupDropdown.values.length > 0) {
selectDefaultDropdownValue(
this._azureResourceGroupDropdown,
this._serviceContext.resourceGroup?.id,
false);
this._azureResourceGroupDropdown.loading = false;
}
} catch (error) {
logError(TelemetryViews.SelectMigrationServiceDialog, '_populateResourceGroupDropdown', error);
void vscode.window.showErrorMessage(
constants.SELECT_RESOURCE_GROUP_ERROR,
error.message);
} finally {
this._azureResourceGroupDropdown.loading = false;
}
}
private async _populateMigrationServiceDropdown(): Promise<void> {
try {
this._azureServiceDropdown.loading = true;
this._azureServiceDropdown.values = await this._getMigrationServiceDropdownValues(
this._serviceContext.azureAccount,
this._serviceContext.subscription,
this._serviceContext.location,
this._serviceContext.resourceGroup);
if (this._azureServiceDropdown.values.length > 0) {
selectDefaultDropdownValue(
this._azureServiceDropdown,
this._serviceContext?.migrationService?.id,
false);
}
} catch (error) {
logError(TelemetryViews.SelectMigrationServiceDialog, '_populateMigrationServiceDropdown', error);
void vscode.window.showErrorMessage(
constants.SELECT_SERVICE_ERROR,
error.message);
} finally {
this._azureServiceDropdown.loading = false;
}
}
private async _getAccountDropdownValues(): Promise<azdata.CategoryValue[]> {
this._azureAccounts = await azdata.accounts.getAllAccounts() || [];
return this._azureAccounts.map(account => {
return {
name: account.displayInfo.userId,
displayName: account.isStale
? constants.ACCOUNT_CREDENTIALS_REFRESH(account.displayInfo.displayName)
: account.displayInfo.displayName,
};
});
}
private async _getSubscriptionDropdownValues(account?: azdata.Account): Promise<azdata.CategoryValue[]> {
this._subscriptions = [];
if (account?.isStale === false) {
try {
this._subscriptions = await getSubscriptions(account);
this._subscriptions.sort((a, b) => a.name.localeCompare(b.name));
} catch (error) {
logError(TelemetryViews.SelectMigrationServiceDialog, '_getSubscriptionDropdownValues', error);
void vscode.window.showErrorMessage(
constants.SELECT_SUBSCRIPTION_ERROR,
error.message);
}
}
return this._subscriptions.map(subscription => {
return {
name: subscription.id,
displayName: `${subscription.name} - ${subscription.id}`,
};
});
}
private _getTenantDropdownValues(account?: azdata.Account): azdata.CategoryValue[] {
this._accountTenants = account?.isStale === false
? account?.properties?.tenants ?? []
: [];
return this._accountTenants.map(tenant => {
return {
name: tenant.id,
displayName: tenant.displayName,
};
});
}
private async _getAzureLocationDropdownValues(
account?: azdata.Account,
subscription?: azureResource.AzureResourceSubscription): Promise<azdata.CategoryValue[]> {
let locations: azureResource.AzureLocation[] = [];
if (account && subscription) {
// get all available locations
locations = await getLocations(account, subscription);
this._sqlMigrationServices = await getSqlMigrationServices(
account,
subscription) || [];
this._sqlMigrationServices.sort((a, b) => a.name.localeCompare(b.name));
} else {
this._sqlMigrationServices = [];
}
// keep locaitons with services only
this._locations = locations.filter(
(loc, i) => this._sqlMigrationServices.some(service => service.location === loc.name));
this._locations.sort((a, b) => a.name.localeCompare(b.name));
return this._locations.map(loc => {
return {
name: loc.name,
displayName: loc.displayName,
};
});
}
private async _getAzureResourceGroupDropdownValues(location?: azureResource.AzureLocation): Promise<azdata.CategoryValue[]> {
this._resourceGroups = location
? this._getMigrationServicesResourceGroups(location)
: [];
this._resourceGroups.sort((a, b) => a.name.localeCompare(b.name));
return this._resourceGroups.map(rg => {
return {
name: rg.id,
displayName: rg.name,
};
});
}
private _getMigrationServicesResourceGroups(location?: azureResource.AzureLocation): azureResource.AzureResourceResourceGroup[] {
const resourceGroups = this._sqlMigrationServices
.filter(service => service.location === location?.name)
.map(service => service.properties.resourceGroup);
return resourceGroups
.filter((rg, i, arr) => arr.indexOf(rg) === i)
.map(rg => {
return <azureResource.AzureResourceResourceGroup>{
id: getFullResourceGroupFromId(rg),
name: rg,
};
});
}
private async _getMigrationServiceDropdownValues(
account?: azdata.Account,
subscription?: azureResource.AzureResourceSubscription,
location?: azureResource.AzureLocation,
resourceGroup?: azureResource.AzureResourceResourceGroup): Promise<azdata.CategoryValue[]> {
const locationName = location?.name?.toLowerCase();
const resourceGroupName = resourceGroup?.name?.toLowerCase();
return this._sqlMigrationServices
.filter(service =>
service.location?.toLowerCase() === locationName &&
service.properties?.resourceGroup?.toLowerCase() === resourceGroupName)
.map(service => {
return ({
name: service.id,
displayName: `${service.name}`,
});
});
}
}

View File

@@ -5,10 +5,10 @@
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, regenerateSqlMigrationServiceAuthKey } from '../../api/azure'; import { DatabaseMigration, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, regenerateSqlMigrationServiceAuthKey } from '../../api/azure';
import { IconPathHelper } from '../../constants/iconPathHelper'; import { IconPathHelper } from '../../constants/iconPathHelper';
import * as constants from '../../constants/strings'; import * as constants from '../../constants/strings';
import { MigrationContext } from '../../models/migrationLocalStorage'; import { MigrationServiceContext } from '../../models/migrationLocalStorage';
import * as styles from '../../constants/styles'; import * as styles from '../../constants/styles';
const CONTROL_MARGIN = '10px'; const CONTROL_MARGIN = '10px';
@@ -28,7 +28,10 @@ export class SqlMigrationServiceDetailsDialog {
private _migrationServiceAuthKeyTable!: azdata.DeclarativeTableComponent; private _migrationServiceAuthKeyTable!: azdata.DeclarativeTableComponent;
private _disposables: vscode.Disposable[] = []; private _disposables: vscode.Disposable[] = [];
constructor(private migrationContext: MigrationContext) { constructor(
private _serviceContext: MigrationServiceContext,
private _migration: DatabaseMigration) {
this._dialog = azdata.window.createModelViewDialog( this._dialog = azdata.window.createModelViewDialog(
'', '',
'SqlMigrationServiceDetailsDialog', 'SqlMigrationServiceDetailsDialog',
@@ -46,7 +49,8 @@ export class SqlMigrationServiceDetailsDialog {
await this.createServiceContent( await this.createServiceContent(
view, view,
this.migrationContext); this._serviceContext,
this._migration);
}); });
this._dialog.okButton.label = constants.SQL_MIGRATION_SERVICE_DETAILS_BUTTON_LABEL; this._dialog.okButton.label = constants.SQL_MIGRATION_SERVICE_DETAILS_BUTTON_LABEL;
@@ -55,33 +59,31 @@ export class SqlMigrationServiceDetailsDialog {
azdata.window.openDialog(this._dialog); azdata.window.openDialog(this._dialog);
} }
private async createServiceContent(view: azdata.ModelView, migrationContext: MigrationContext): Promise<void> { private async createServiceContent(view: azdata.ModelView, serviceContext: MigrationServiceContext, migration: DatabaseMigration): Promise<void> {
this._migrationServiceAuthKeyTable = this._createIrTable(view); this._migrationServiceAuthKeyTable = this._createIrTable(view);
const serviceNode = (await getSqlMigrationServiceMonitoringData( const serviceNode = (await getSqlMigrationServiceMonitoringData(
migrationContext.azureAccount, serviceContext.azureAccount!,
migrationContext.subscription, serviceContext.subscription!,
migrationContext.controller.properties.resourceGroup, serviceContext.migrationService?.properties.resourceGroup!,
migrationContext.controller.location, serviceContext.migrationService?.location!,
migrationContext.controller.name, serviceContext.migrationService?.name!));
this.migrationContext.sessionId!
));
const serviceNodeName = serviceNode.nodes?.map(node => node.nodeName).join(', ') const serviceNodeName = serviceNode.nodes?.map(node => node.nodeName).join(', ')
|| constants.SQL_MIGRATION_SERVICE_DETAILS_STATUS_UNAVAILABLE; || constants.SQL_MIGRATION_SERVICE_DETAILS_STATUS_UNAVAILABLE;
const flexContainer = view.modelBuilder const flexContainer = view.modelBuilder
.flexContainer() .flexContainer()
.withItems([ .withItems([
this._createHeading(view, migrationContext), this._createHeading(view, this._migration),
view.modelBuilder view.modelBuilder
.separator() .separator()
.withProps({ width: STRETCH_WIDTH }) .withProps({ width: STRETCH_WIDTH })
.component(), .component(),
this._createTextItem(view, constants.SUBSCRIPTION, LABEL_MARGIN), this._createTextItem(view, constants.SUBSCRIPTION, LABEL_MARGIN),
this._createTextItem(view, migrationContext.subscription.name, VALUE_MARGIN), this._createTextItem(view, serviceContext.subscription?.name!, VALUE_MARGIN),
this._createTextItem(view, constants.LOCATION, LABEL_MARGIN), this._createTextItem(view, constants.LOCATION, LABEL_MARGIN),
this._createTextItem(view, migrationContext.controller.location.toUpperCase(), VALUE_MARGIN), this._createTextItem(view, serviceContext.migrationService?.location?.toUpperCase()!, VALUE_MARGIN),
this._createTextItem(view, constants.RESOURCE_GROUP, LABEL_MARGIN), this._createTextItem(view, constants.RESOURCE_GROUP, LABEL_MARGIN),
this._createTextItem(view, migrationContext.controller.properties.resourceGroup, VALUE_MARGIN), this._createTextItem(view, serviceContext.migrationService?.properties.resourceGroup!, VALUE_MARGIN),
this._createTextItem(view, constants.SQL_MIGRATION_SERVICE_DETAILS_IR_LABEL, LABEL_MARGIN), this._createTextItem(view, constants.SQL_MIGRATION_SERVICE_DETAILS_IR_LABEL, LABEL_MARGIN),
this._createTextItem(view, serviceNodeName, VALUE_MARGIN), this._createTextItem(view, serviceNodeName, VALUE_MARGIN),
this._createTextItem( this._createTextItem(
@@ -96,10 +98,10 @@ export class SqlMigrationServiceDetailsDialog {
.component(); .component();
await view.initializeModel(flexContainer); await view.initializeModel(flexContainer);
return await this._refreshAuthTable(view, migrationContext); return await this._refreshAuthTable(view, serviceContext, migration);
} }
private _createHeading(view: azdata.ModelView, migrationContext: MigrationContext): azdata.FlexContainer { private _createHeading(view: azdata.ModelView, migration: DatabaseMigration): azdata.FlexContainer {
return view.modelBuilder return view.modelBuilder
.flexContainer() .flexContainer()
.withItems([ .withItems([
@@ -120,19 +122,15 @@ export class SqlMigrationServiceDetailsDialog {
view.modelBuilder view.modelBuilder
.text() .text()
.withProps({ .withProps({
value: migrationContext.controller.name, value: this._serviceContext.migrationService?.name,
CSSStyles: { CSSStyles: { ...styles.SECTION_HEADER_CSS }
...styles.SECTION_HEADER_CSS
}
}) })
.component(), .component(),
view.modelBuilder view.modelBuilder
.text() .text()
.withProps({ .withProps({
value: constants.SQL_MIGRATION_SERVICE_DETAILS_SUB_TITLE, value: constants.SQL_MIGRATION_SERVICE_DETAILS_SUB_TITLE,
CSSStyles: { CSSStyles: { ...styles.SMALL_NOTE_CSS }
...styles.SMALL_NOTE_CSS
}
}) })
.component(), .component(),
]) ])
@@ -197,15 +195,14 @@ export class SqlMigrationServiceDetailsDialog {
}; };
} }
private async _regenerateAuthKey(view: azdata.ModelView, migrationContext: MigrationContext, keyName: string): Promise<void> { private async _regenerateAuthKey(view: azdata.ModelView, serviceContext: MigrationServiceContext, migration: DatabaseMigration, keyName: string): Promise<void> {
const keys = await regenerateSqlMigrationServiceAuthKey( const keys = await regenerateSqlMigrationServiceAuthKey(
migrationContext.azureAccount, serviceContext.azureAccount!,
migrationContext.subscription, serviceContext.subscription!,
migrationContext.controller.properties.resourceGroup, serviceContext.migrationService?.properties.resourceGroup!,
migrationContext.controller.location.toUpperCase(), serviceContext.migrationService?.properties.location?.toUpperCase()!,
migrationContext.controller.name, serviceContext.migrationService?.name!,
keyName, keyName);
migrationContext.sessionId!);
if (keys?.authKey1 && keyName === AUTH_KEY1) { if (keys?.authKey1 && keyName === AUTH_KEY1) {
await this._updateTableCell(this._migrationServiceAuthKeyTable, 0, 1, keys.authKey1, constants.SERVICE_KEY1_LABEL); await this._updateTableCell(this._migrationServiceAuthKeyTable, 0, 1, keys.authKey1, constants.SERVICE_KEY1_LABEL);
@@ -223,14 +220,13 @@ export class SqlMigrationServiceDetailsDialog {
await vscode.window.showInformationMessage(constants.AUTH_KEY_REFRESHED(keyName)); await vscode.window.showInformationMessage(constants.AUTH_KEY_REFRESHED(keyName));
} }
private async _refreshAuthTable(view: azdata.ModelView, migrationContext: MigrationContext): Promise<void> { private async _refreshAuthTable(view: azdata.ModelView, serviceContext: MigrationServiceContext, migration: DatabaseMigration): Promise<void> {
const keys = await getSqlMigrationServiceAuthKeys( const keys = await getSqlMigrationServiceAuthKeys(
migrationContext.azureAccount, serviceContext.azureAccount!,
migrationContext.subscription, serviceContext.subscription!,
migrationContext.controller.properties.resourceGroup, serviceContext.migrationService?.properties.resourceGroup!,
migrationContext.controller.location.toUpperCase(), serviceContext.migrationService?.location.toUpperCase()!,
migrationContext.controller.name, serviceContext.migrationService?.name!);
migrationContext.sessionId!);
const copyKey1Button = view.modelBuilder const copyKey1Button = view.modelBuilder
.button() .button()
@@ -275,7 +271,7 @@ export class SqlMigrationServiceDetailsDialog {
}) })
.component(); .component();
this._disposables.push(refreshKey1Button.onDidClick( this._disposables.push(refreshKey1Button.onDidClick(
async (e) => await this._regenerateAuthKey(view, migrationContext, AUTH_KEY1))); async (e) => await this._regenerateAuthKey(view, serviceContext, migration, AUTH_KEY1)));
const refreshKey2Button = view.modelBuilder const refreshKey2Button = view.modelBuilder
.button() .button()
@@ -288,7 +284,7 @@ export class SqlMigrationServiceDetailsDialog {
}) })
.component(); .component();
this._disposables.push(refreshKey2Button.onDidClick( this._disposables.push(refreshKey2Button.onDidClick(
async (e) => await this._regenerateAuthKey(view, migrationContext, AUTH_KEY2))); async (e) => await this._regenerateAuthKey(view, serviceContext, migration, AUTH_KEY2)));
await this._migrationServiceAuthKeyTable.updateProperties({ await this._migrationServiceAuthKeyTable.updateProperties({
dataValues: [ dataValues: [

View File

@@ -32,47 +32,54 @@ class SQLMigration {
async registerCommands(): Promise<void> { async registerCommands(): Promise<void> {
const commandDisposables: vscode.Disposable[] = [ // Array of disposables returned by registerCommand const commandDisposables: vscode.Disposable[] = [ // Array of disposables returned by registerCommand
vscode.commands.registerCommand('sqlmigration.start', async () => { vscode.commands.registerCommand(
await this.launchMigrationWizard(); 'sqlmigration.start',
}), async () => await this.launchMigrationWizard()),
vscode.commands.registerCommand('sqlmigration.openNotebooks', async () => { vscode.commands.registerCommand(
const input = vscode.window.createQuickPick<MigrationNotebookInfo>(); 'sqlmigration.openNotebooks',
input.placeholder = loc.NOTEBOOK_QUICK_PICK_PLACEHOLDER; async () => {
const input = vscode.window.createQuickPick<MigrationNotebookInfo>();
input.placeholder = loc.NOTEBOOK_QUICK_PICK_PLACEHOLDER;
input.items = NotebookPathHelper.getAllMigrationNotebooks(); input.items = NotebookPathHelper.getAllMigrationNotebooks();
this.context.subscriptions.push(input.onDidAccept(async (e) => { this.context.subscriptions.push(input.onDidAccept(async (e) => {
const selectedNotebook = input.selectedItems[0]; const selectedNotebook = input.selectedItems[0];
if (selectedNotebook) { if (selectedNotebook) {
try { try {
await azdata.nb.showNotebookDocument(vscode.Uri.parse(`untitled: ${selectedNotebook.label}`), { await azdata.nb.showNotebookDocument(vscode.Uri.parse(`untitled: ${selectedNotebook.label}`), {
preview: false, preview: false,
initialContent: (await fs.readFile(selectedNotebook.notebookPath)).toString(), initialContent: (await fs.readFile(selectedNotebook.notebookPath)).toString(),
initialDirtyState: false initialDirtyState: false
}); });
} catch (e) { } catch (e) {
void vscode.window.showErrorMessage(`${loc.NOTEBOOK_OPEN_ERROR} - ${e.toString()}`); void vscode.window.showErrorMessage(`${loc.NOTEBOOK_OPEN_ERROR} - ${e.toString()}`);
}
input.hide();
} }
input.hide(); }));
}
}));
input.show(); input.show();
}), }),
azdata.tasks.registerTask('sqlmigration.start', async () => { azdata.tasks.registerTask(
await this.launchMigrationWizard(); 'sqlmigration.start',
}), async () => await this.launchMigrationWizard()),
azdata.tasks.registerTask('sqlmigration.newsupportrequest', async () => { azdata.tasks.registerTask(
await this.launchNewSupportRequest(); 'sqlmigration.newsupportrequest',
}), async () => await this.launchNewSupportRequest()),
azdata.tasks.registerTask('sqlmigration.sendfeedback', async () => { azdata.tasks.registerTask(
const actionId = 'workbench.action.openIssueReporter'; 'sqlmigration.sendfeedback',
const args = { async () => {
extensionId: 'microsoft.sql-migration', const actionId = 'workbench.action.openIssueReporter';
issueTitle: loc.FEEDBACK_ISSUE_TITLE, const args = {
}; extensionId: 'microsoft.sql-migration',
return await vscode.commands.executeCommand(actionId, args); issueTitle: loc.FEEDBACK_ISSUE_TITLE,
}), };
return await vscode.commands.executeCommand(actionId, args);
}),
azdata.tasks.registerTask(
'sqlmigration.refreshmigrations',
async (e) => await widget?.refreshMigrations()),
]; ];
this.context.subscriptions.push(...commandDisposables); this.context.subscriptions.push(...commandDisposables);
@@ -97,14 +104,20 @@ class SQLMigration {
if (api) { if (api) {
this.stateModel = new MigrationStateModel(this.context, connectionId, api.sqlMigration); this.stateModel = new MigrationStateModel(this.context, connectionId, api.sqlMigration);
this.context.subscriptions.push(this.stateModel); this.context.subscriptions.push(this.stateModel);
let savedInfo = this.checkSavedInfo(serverName); const savedInfo = this.checkSavedInfo(serverName);
if (savedInfo) { if (savedInfo) {
this.stateModel.savedInfo = savedInfo; this.stateModel.savedInfo = savedInfo;
this.stateModel.serverName = serverName; this.stateModel.serverName = serverName;
let savedAssessmentDialog = new SavedAssessmentDialog(this.context, this.stateModel); const savedAssessmentDialog = new SavedAssessmentDialog(
this.context,
this.stateModel,
async () => await widget?.onDialogClosed());
await savedAssessmentDialog.openDialog(); await savedAssessmentDialog.openDialog();
} else { } else {
const wizardController = new WizardController(this.context, this.stateModel); const wizardController = new WizardController(
this.context,
this.stateModel,
async () => await widget?.onDialogClosed());
await wizardController.openWizard(connectionId); await wizardController.openWizard(connectionId);
} }
} }
@@ -131,10 +144,11 @@ class SQLMigration {
} }
let sqlMigration: SQLMigration; let sqlMigration: SQLMigration;
let widget: DashboardWidget;
export async function activate(context: vscode.ExtensionContext) { export async function activate(context: vscode.ExtensionContext) {
sqlMigration = new SQLMigration(context); sqlMigration = new SQLMigration(context);
await sqlMigration.registerCommands(); await sqlMigration.registerCommands();
let widget = new DashboardWidget(context); widget = new DashboardWidget(context);
widget.register(); widget.register();
} }

View File

@@ -2,11 +2,13 @@
* Copyright (c) Microsoft Corporation. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { azureResource } from 'azureResource';
import { logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../telemtery';
import { DatabaseMigration, SqlMigrationService, SqlManagedInstance, getMigrationStatus, AzureAsyncOperationResource, getMigrationAsyncOperationDetails, SqlVMServer, getSubscriptions } from '../api/azure';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as azurecore from 'azurecore';
import { azureResource } from 'azureResource';
import { DatabaseMigration, SqlMigrationService, getSubscriptions, getServiceMigrations } from '../api/azure';
import { deepClone } from '../api/utils';
import * as loc from '../constants/strings';
export class MigrationLocalStorage { export class MigrationLocalStorage {
private static context: vscode.ExtensionContext; private static context: vscode.ExtensionContext;
@@ -16,161 +18,75 @@ export class MigrationLocalStorage {
MigrationLocalStorage.context = context; MigrationLocalStorage.context = context;
} }
public static async getMigrationsBySourceConnections(connectionProfile: azdata.connection.ConnectionProfile, refreshStatus?: boolean): Promise<MigrationContext[]> { public static async getMigrationServiceContext(): Promise<MigrationServiceContext> {
const undefinedSessionId = '{undefined}'; const connectionProfile = await azdata.connection.getCurrentConnection();
const result: MigrationContext[] = []; if (connectionProfile) {
const validMigrations: MigrationContext[] = []; const serverContextKey = `${this.mementoToken}.${connectionProfile.serverName}.serviceContext`;
const startTime = new Date().toString(); return deepClone(await this.context.globalState.get(serverContextKey)) || {};
// fetch saved migrations
const migrationMementos: MigrationContext[] = this.context.globalState.get(this.mementoToken) || [];
for (let i = 0; i < migrationMementos.length; i++) {
const migration = migrationMementos[i];
migration.migrationContext = this.removeMigrationSecrets(migration.migrationContext);
migration.sessionId = migration.sessionId ?? undefinedSessionId;
if (migration.sourceConnectionProfile.serverName === connectionProfile.serverName) {
// refresh migration status
if (refreshStatus) {
try {
await this.refreshMigrationAzureAccount(migration);
if (migration.asyncUrl) {
migration.asyncOperationResult = await getMigrationAsyncOperationDetails(
migration.azureAccount,
migration.subscription,
migration.asyncUrl,
migration.sessionId!);
}
migration.migrationContext = await getMigrationStatus(
migration.azureAccount,
migration.subscription,
migration.migrationContext,
migration.sessionId!);
}
catch (e) {
// Keeping only valid migrations in cache. Clearing all the migrations which return ResourceDoesNotExit error.
switch (e.message) {
case 'ResourceDoesNotExist':
case 'NullMigrationId':
continue;
default:
logError(TelemetryViews.MigrationLocalStorage, 'MigrationBySourceConnectionError', e);
}
}
}
result.push(migration);
}
validMigrations.push(migration);
} }
return {};
await this.context.globalState.update(this.mementoToken, validMigrations);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationLocalStorage,
TelemetryAction.Done,
{
'startTime': startTime,
'endTime': new Date().toString()
},
{
'migrationCount': migrationMementos.length
}
);
// only save updated migration context
if (refreshStatus) {
const migrations: MigrationContext[] = this.context.globalState.get(this.mementoToken) || [];
validMigrations.forEach(migration => {
const idx = migrations.findIndex(m => m.migrationContext.id === migration.migrationContext.id);
if (idx > -1) {
migrations[idx] = migration;
}
});
// check global state for migrations count mismatch, avoid saving
// state if the count has changed when a migration may have been added
const current: MigrationContext[] = this.context.globalState.get(this.mementoToken) || [];
if (current.length === migrations.length) {
await this.context.globalState.update(this.mementoToken, migrations);
}
}
return result;
} }
public static async refreshMigrationAzureAccount(migration: MigrationContext): Promise<void> { public static async saveMigrationServiceContext(serviceContext: MigrationServiceContext): Promise<void> {
if (migration.azureAccount.isStale) { const connectionProfile = await azdata.connection.getCurrentConnection();
if (connectionProfile) {
const serverContextKey = `${this.mementoToken}.${connectionProfile.serverName}.serviceContext`;
return await this.context.globalState.update(serverContextKey, deepClone(serviceContext));
}
}
public static async refreshMigrationAzureAccount(serviceContext: MigrationServiceContext, migration: DatabaseMigration): Promise<void> {
if (serviceContext.azureAccount?.isStale) {
const accounts = await azdata.accounts.getAllAccounts(); const accounts = await azdata.accounts.getAllAccounts();
const account = accounts.find(a => !a.isStale && a.key.accountId === migration.azureAccount.key.accountId); const account = accounts.find(a => !a.isStale && a.key.accountId === serviceContext.azureAccount?.key.accountId);
if (account) { if (account) {
const subscriptions = await getSubscriptions(account); const subscriptions = await getSubscriptions(account);
const subscription = subscriptions.find(s => s.id === migration.subscription.id); const subscription = subscriptions.find(s => s.id === serviceContext.subscription?.id);
if (subscription) { if (subscription) {
migration.azureAccount = account; serviceContext.azureAccount = account;
await this.saveMigrationServiceContext(serviceContext);
} }
} }
} }
} }
public static async saveMigration(
connectionProfile: azdata.connection.ConnectionProfile,
migrationContext: DatabaseMigration,
targetMI: SqlManagedInstance | SqlVMServer,
azureAccount: azdata.Account,
subscription: azureResource.AzureResourceSubscription,
controller: SqlMigrationService,
asyncURL: string,
sessionId: string): Promise<void> {
try {
let migrationMementos: MigrationContext[] = this.context.globalState.get(this.mementoToken) || [];
migrationMementos = migrationMementos.filter(m => m.migrationContext.id !== migrationContext.id);
migrationMementos.push({
sourceConnectionProfile: connectionProfile,
migrationContext: this.removeMigrationSecrets(migrationContext),
targetManagedInstance: targetMI,
subscription: subscription,
azureAccount: azureAccount,
controller: controller,
asyncUrl: asyncURL,
sessionId: sessionId
});
await this.context.globalState.update(this.mementoToken, migrationMementos);
} catch (e) {
logError(TelemetryViews.MigrationLocalStorage, 'CantSaveMigration', e);
}
}
public static async clearMigrations(): Promise<void> {
await this.context.globalState.update(this.mementoToken, ([] as MigrationContext[]));
}
public static removeMigrationSecrets(migration: DatabaseMigration): DatabaseMigration {
// remove secrets from migration context
if (migration.properties.sourceSqlConnection?.password) {
migration.properties.sourceSqlConnection.password = '';
}
if (migration.properties.backupConfiguration?.sourceLocation?.fileShare?.password) {
migration.properties.backupConfiguration.sourceLocation.fileShare.password = '';
}
if (migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.accountKey) {
migration.properties.backupConfiguration.sourceLocation.azureBlob.accountKey = '';
}
if (migration.properties.backupConfiguration?.targetLocation?.accountKey) {
migration.properties.backupConfiguration.targetLocation.accountKey = '';
}
return migration;
}
} }
export interface MigrationContext { export function isServiceContextValid(serviceContext: MigrationServiceContext): boolean {
sourceConnectionProfile: azdata.connection.ConnectionProfile, return (
migrationContext: DatabaseMigration, serviceContext.azureAccount?.isStale === false &&
targetManagedInstance: SqlManagedInstance | SqlVMServer, serviceContext.location?.id !== undefined &&
azureAccount: azdata.Account, serviceContext.migrationService?.id !== undefined &&
subscription: azureResource.AzureResourceSubscription, serviceContext.resourceGroup?.id !== undefined &&
controller: SqlMigrationService, serviceContext.subscription?.id !== undefined &&
asyncUrl: string, serviceContext.tenant?.id !== undefined
asyncOperationResult?: AzureAsyncOperationResource, );
sessionId?: string }
export async function getSelectedServiceStatus(): Promise<string> {
const serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
const serviceName = serviceContext?.migrationService?.name;
return serviceName && isServiceContextValid(serviceContext)
? loc.MIGRATION_SERVICE_SERVICE_PROMPT(serviceName)
: loc.MIGRATION_SERVICE_SELECT_SERVICE_PROMPT;
}
export async function getCurrentMigrations(): Promise<DatabaseMigration[]> {
const serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
return isServiceContextValid(serviceContext)
? await getServiceMigrations(
serviceContext.azureAccount!,
serviceContext.subscription!,
serviceContext.migrationService?.id!)
: [];
}
export interface MigrationServiceContext {
azureAccount?: azdata.Account,
tenant?: azurecore.Tenant,
subscription?: azureResource.AzureResourceSubscription,
location?: azureResource.AzureLocation,
resourceGroup?: azureResource.AzureResourceResourceGroup,
migrationService?: SqlMigrationService,
} }
export enum MigrationStatus { export enum MigrationStatus {

View File

@@ -8,9 +8,8 @@ import { azureResource } from 'azureResource';
import * as azurecore from 'azurecore'; import * as azurecore from 'azurecore';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as mssql from 'mssql'; import * as mssql from 'mssql';
import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getSqlMigrationServices, getSubscriptions, SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer, getLocations, getLocationDisplayName, getSqlManagedInstanceDatabases, getBlobs, sortResourceArrayByName, getFullResourceGroupFromId, getResourceGroupFromId, getResourceGroups } from '../api/azure'; import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getSqlMigrationServices, getSubscriptions, SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer, getLocations, getLocationDisplayName, getSqlManagedInstanceDatabases, getBlobs, sortResourceArrayByName, getFullResourceGroupFromId, getResourceGroupFromId, getResourceGroups, getSqlMigrationServicesByResourceGroup } from '../api/azure';
import * as constants from '../constants/strings'; import * as constants from '../constants/strings';
import { MigrationLocalStorage } from './migrationLocalStorage';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery'; import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery';
@@ -58,6 +57,12 @@ export enum NetworkContainerType {
NETWORK_SHARE NETWORK_SHARE
} }
export enum FileStorageType {
FileShare = 'FileShare',
AzureBlob = 'AzureBlob',
None = 'None',
}
export enum Page { export enum Page {
DatabaseSelector, DatabaseSelector,
SKURecommendation, SKURecommendation,
@@ -826,8 +831,10 @@ export class MigrationStateModel implements Model, vscode.Disposable {
} }
accountValues = this._azureAccounts.map((account): azdata.CategoryValue => { accountValues = this._azureAccounts.map((account): azdata.CategoryValue => {
return { return {
displayName: account.displayInfo.displayName, name: account.displayInfo.userId,
name: account.displayInfo.userId displayName: account.isStale
? constants.ACCOUNT_CREDENTIALS_REFRESH(account.displayInfo.displayName)
: account.displayInfo.displayName
}; };
}); });
} catch (e) { } catch (e) {
@@ -871,7 +878,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public async getSubscriptionsDropdownValues(): Promise<azdata.CategoryValue[]> { public async getSubscriptionsDropdownValues(): Promise<azdata.CategoryValue[]> {
let subscriptionsValues: azdata.CategoryValue[] = []; let subscriptionsValues: azdata.CategoryValue[] = [];
try { try {
if (this._azureAccount) { if (this._azureAccount?.isStale === false) {
this._subscriptions = await getSubscriptions(this._azureAccount); this._subscriptions = await getSubscriptions(this._azureAccount);
} else { } else {
this._subscriptions = []; this._subscriptions = [];
@@ -1471,7 +1478,12 @@ export class MigrationStateModel implements Model, vscode.Disposable {
let sqlMigrationServiceValues: azdata.CategoryValue[] = []; let sqlMigrationServiceValues: azdata.CategoryValue[] = [];
try { try {
if (this._azureAccount && subscription && resourceGroupName && this._targetServerInstance) { if (this._azureAccount && subscription && resourceGroupName && this._targetServerInstance) {
this._sqlMigrationServices = (await getSqlMigrationServices(this._azureAccount, subscription)).filter(sms => sms.location.toLowerCase() === this._targetServerInstance.location.toLowerCase() && sms.properties.resourceGroup.toLowerCase() === resourceGroupName.toLowerCase()); const services = await getSqlMigrationServicesByResourceGroup(
this._azureAccount,
subscription,
resourceGroupName?.toLowerCase());
const targetLoc = this._targetServerInstance.location.toLowerCase();
this._sqlMigrationServices = services.filter(sms => sms.location.toLowerCase() === targetLoc);
} else { } else {
this._sqlMigrationServices = []; this._sqlMigrationServices = [];
} }
@@ -1545,6 +1557,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
requestBody.properties.backupConfiguration = { requestBody.properties.backupConfiguration = {
targetLocation: undefined!, targetLocation: undefined!,
sourceLocation: { sourceLocation: {
fileStorageType: 'AzureBlob',
azureBlob: { azureBlob: {
storageAccountResourceId: this._databaseBackup.blobs[i].storageAccount.id, storageAccountResourceId: this._databaseBackup.blobs[i].storageAccount.id,
accountKey: this._databaseBackup.blobs[i].storageKey, accountKey: this._databaseBackup.blobs[i].storageKey,
@@ -1567,6 +1580,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
accountKey: this._databaseBackup.networkShares[i].storageKey, accountKey: this._databaseBackup.networkShares[i].storageKey,
}, },
sourceLocation: { sourceLocation: {
fileStorageType: 'FileShare',
fileShare: { fileShare: {
path: this._databaseBackup.networkShares[i].networkShareLocation, path: this._databaseBackup.networkShares[i].networkShareLocation,
username: this._databaseBackup.networkShares[i].windowsUser, username: this._databaseBackup.networkShares[i].windowsUser,
@@ -1584,8 +1598,8 @@ export class MigrationStateModel implements Model, vscode.Disposable {
this._targetServerInstance, this._targetServerInstance,
this._targetDatabaseNames[i], this._targetDatabaseNames[i],
requestBody, requestBody,
this._sessionId this._sessionId);
);
response.databaseMigration.properties.sourceDatabaseName = this._databasesForMigration[i]; response.databaseMigration.properties.sourceDatabaseName = this._databasesForMigration[i];
response.databaseMigration.properties.backupConfiguration = requestBody.properties.backupConfiguration!; response.databaseMigration.properties.backupConfiguration = requestBody.properties.backupConfiguration!;
response.databaseMigration.properties.offlineConfiguration = requestBody.properties.offlineConfiguration!; response.databaseMigration.properties.offlineConfiguration = requestBody.properties.offlineConfiguration!;
@@ -1621,22 +1635,18 @@ export class MigrationStateModel implements Model, vscode.Disposable {
} }
); );
await MigrationLocalStorage.saveMigration( void vscode.window.showInformationMessage(
currentConnection!, localize(
response.databaseMigration, "sql.migration.starting.migration.message",
this._targetServerInstance, 'Starting migration for database {0} to {1} - {2}',
this._azureAccount, this._databasesForMigration[i],
this._targetSubscription, this._targetServerInstance.name,
this._sqlMigrationService!, this._targetDatabaseNames[i]));
response.asyncUrl,
this._sessionId
);
void vscode.window.showInformationMessage(localize("sql.migration.starting.migration.message", 'Starting migration for database {0} to {1} - {2}', this._databasesForMigration[i], this._targetServerInstance.name, this._targetDatabaseNames[i]));
} }
} catch (e) { } catch (e) {
void vscode.window.showErrorMessage( void vscode.window.showErrorMessage(
localize('sql.migration.starting.migration.error', "An error occurred while starting the migration: '{0}'", e.message)); localize('sql.migration.starting.migration.error', "An error occurred while starting the migration: '{0}'", e.message));
console.log(e); logError(TelemetryViews.MigrationLocalStorage, 'StartMigrationFailed', e);
} }
finally { finally {
// kill existing data collection if user start migration // kill existing data collection if user start migration
@@ -1718,7 +1728,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
} }
} }
public loadSavedInfo(): Boolean { public async loadSavedInfo(): Promise<Boolean> {
try { try {
this._targetType = this.savedInfo.migrationTargetType || undefined!; this._targetType = this.savedInfo.migrationTargetType || undefined!;

View File

@@ -28,7 +28,8 @@ export enum TelemetryViews {
SqlMigrationWizard = 'SqlMigrationWizard', SqlMigrationWizard = 'SqlMigrationWizard',
MigrationLocalStorage = 'MigrationLocalStorage', MigrationLocalStorage = 'MigrationLocalStorage',
SkuRecommendationWizard = 'SkuRecommendationWizard', SkuRecommendationWizard = 'SkuRecommendationWizard',
DataCollectionWizard = 'GetAzureRecommendationDialog' DataCollectionWizard = 'GetAzureRecommendationDialog',
SelectMigrationServiceDialog = 'SelectMigrationServiceDialog',
} }
export enum TelemetryAction { export enum TelemetryAction {

View File

@@ -786,15 +786,27 @@ export class DatabaseBackupPage extends MigrationWizardPage {
await this.switchNetworkContainerFields(this.migrationStateModel._databaseBackup.networkContainerType); await this.switchNetworkContainerFields(this.migrationStateModel._databaseBackup.networkContainerType);
const connectionProfile = await this.migrationStateModel.getSourceConnectionProfile(); const connectionProfile = await this.migrationStateModel.getSourceConnectionProfile();
const queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>((await this.migrationStateModel.getSourceConnectionProfile()).providerId, azdata.DataProviderType.QueryProvider); const queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(
(await this.migrationStateModel.getSourceConnectionProfile()).providerId,
azdata.DataProviderType.QueryProvider);
const query = 'select SUSER_NAME()'; const query = 'select SUSER_NAME()';
const results = await queryProvider.runQueryAndReturn(await (azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId)), query); const results = await queryProvider.runQueryAndReturn(
await (azdata.connection.getUriForConnection(
this.migrationStateModel.sourceConnectionId)), query);
const username = results.rows[0][0].displayValue; const username = results.rows[0][0].displayValue;
this.migrationStateModel._authenticationType = connectionProfile.authenticationType === 'SqlLogin' ? MigrationSourceAuthenticationType.Sql : connectionProfile.authenticationType === 'Integrated' ? MigrationSourceAuthenticationType.Integrated : undefined!; this.migrationStateModel._authenticationType = connectionProfile.authenticationType === 'SqlLogin'
? MigrationSourceAuthenticationType.Sql
: connectionProfile.authenticationType === 'Integrated'
? MigrationSourceAuthenticationType.Integrated
: undefined!;
this._sourceHelpText.value = constants.SQL_SOURCE_DETAILS(this.migrationStateModel._authenticationType, connectionProfile.serverName); this._sourceHelpText.value = constants.SQL_SOURCE_DETAILS(this.migrationStateModel._authenticationType, connectionProfile.serverName);
this._sqlSourceUsernameInput.value = username; this._sqlSourceUsernameInput.value = username;
this._sqlSourcePassword.value = (await azdata.connection.getCredentials(this.migrationStateModel.sourceConnectionId)).password; this._sqlSourcePassword.value = (await azdata.connection.getCredentials(this.migrationStateModel.sourceConnectionId)).password;
this._windowsUserAccountText.value = this.migrationStateModel.savedInfo?.networkShares[0]?.windowsUser; this._windowsUserAccountText.value = this.migrationStateModel.savedInfo?.networkShares
? this.migrationStateModel.savedInfo?.networkShares[0]?.windowsUser
: '';
this._networkShareTargetDatabaseNames = []; this._networkShareTargetDatabaseNames = [];
this._networkShareLocations = []; this._networkShareLocations = [];
@@ -809,7 +821,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
} }
let originalTargetDatabaseNames = this.migrationStateModel._targetDatabaseNames; let originalTargetDatabaseNames = this.migrationStateModel._targetDatabaseNames;
let originalNetworkShares = this.migrationStateModel._databaseBackup.networkShares; let originalNetworkShares = this.migrationStateModel._databaseBackup.networkShares || [];
let originalBlobs = this.migrationStateModel._databaseBackup.blobs; let originalBlobs = this.migrationStateModel._databaseBackup.blobs;
if (this.migrationStateModel._didUpdateDatabasesForMigration) { if (this.migrationStateModel._didUpdateDatabasesForMigration) {
this.migrationStateModel._targetDatabaseNames = []; this.migrationStateModel._targetDatabaseNames = [];
@@ -830,7 +842,10 @@ export class DatabaseBackupPage extends MigrationWizardPage {
blob = originalBlobs[dbIndex] ?? blob; blob = originalBlobs[dbIndex] ?? blob;
} else { } else {
// network share values are uniform for all dbs in the same migration, except for networkShareLocation // network share values are uniform for all dbs in the same migration, except for networkShareLocation
const previouslySelectedNetworkShare = originalNetworkShares[0]; const previouslySelectedNetworkShare = originalNetworkShares.length > 0
? originalNetworkShares[0]
: '';
if (previouslySelectedNetworkShare) { if (previouslySelectedNetworkShare) {
networkShare = { networkShare = {
...previouslySelectedNetworkShare, ...previouslySelectedNetworkShare,

View File

@@ -8,43 +8,16 @@ import * as vscode from 'vscode';
import { MigrationWizardPage } from '../models/migrationWizardPage'; import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../constants/strings'; import * as constants from '../constants/strings';
import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
import { debounce } from '../api/utils'; import { debounce } from '../api/utils';
import * as styles from '../constants/styles'; import * as styles from '../constants/styles';
import { selectDatabasesFromList } from '../constants/helper'; import { IconPathHelper } from '../constants/iconPathHelper';
const styleLeft: azdata.CssStyles = {
'border': 'none',
'text-align': 'left',
'white-space': 'nowrap',
'text-overflow': 'ellipsis',
'overflow': 'hidden',
'box-shadow': '0px -1px 0px 0px rgba(243, 242, 241, 1) inset'
};
const styleCheckBox: azdata.CssStyles = {
'border': 'none',
'text-align': 'left',
'white-space': 'nowrap',
'text-overflow': 'ellipsis',
'overflow': 'hidden',
};
const styleRight: azdata.CssStyles = {
'border': 'none',
'text-align': 'right',
'white-space': 'nowrap',
'text-overflow': 'ellipsis',
'overflow': 'hidden',
'box-shadow': '0px -1px 0px 0px rgba(243, 242, 241, 1) inset'
};
export class DatabaseSelectorPage extends MigrationWizardPage { export class DatabaseSelectorPage extends MigrationWizardPage {
private _view!: azdata.ModelView; private _view!: azdata.ModelView;
private _databaseSelectorTable!: azdata.DeclarativeTableComponent; private _databaseSelectorTable!: azdata.TableComponent;
private _dbNames!: string[]; private _dbNames!: string[];
private _dbCount!: azdata.TextComponent; private _dbCount!: azdata.TextComponent;
private _databaseTableValues!: azdata.DeclarativeTableCellValue[][]; private _databaseTableValues!: any[];
private _disposables: vscode.Disposable[] = []; private _disposables: vscode.Disposable[] = [];
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
@@ -115,7 +88,6 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
protected async handleStateChange(e: StateChangeEvent): Promise<void> { protected async handleStateChange(e: StateChangeEvent): Promise<void> {
} }
private createSearchComponent(): azdata.DivContainer { private createSearchComponent(): azdata.DivContainer {
let resourceSearchBox = this._view.modelBuilder.inputBox().withProps({ let resourceSearchBox = this._view.modelBuilder.inputBox().withProps({
stopEnterPropagation: true, stopEnterPropagation: true,
@@ -137,74 +109,36 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
} }
@debounce(500) @debounce(500)
private _filterTableList(value: string): void { private async _filterTableList(value: string, selectedList?: string[]): Promise<void> {
const selectedRows: number[] = [];
const selectedDatabases = selectedList || this.selectedDbs();
let tableRows = this._databaseTableValues;
if (this._databaseTableValues && value?.length > 0) { if (this._databaseTableValues && value?.length > 0) {
const filter: number[] = []; tableRows = this._databaseTableValues
this._databaseTableValues.forEach((row, index) => { .filter(row => {
// undo when bug #16445 is fixed const searchText = value?.toLowerCase();
// const flexContainer: azdata.FlexContainer = row[1]?.value as azdata.FlexContainer; return row[2]?.toLowerCase()?.indexOf(searchText) > -1 // database name
// const textComponent: azdata.TextComponent = flexContainer?.items[1] as azdata.TextComponent; || row[3]?.toLowerCase()?.indexOf(searchText) > -1 // state
// const cellText = textComponent?.value?.toLowerCase(); || row[4]?.toLowerCase()?.indexOf(searchText) > -1 // size
const text = row[1]?.value as string; || row[5]?.toLowerCase()?.indexOf(searchText) > -1; // last backup date
const cellText = text?.toLowerCase(); });
const searchText: string = value?.toLowerCase();
if (cellText?.includes(searchText)) {
filter.push(index);
}
});
this._databaseSelectorTable.setFilter(filter);
} else {
this._databaseSelectorTable.setFilter(undefined);
} }
for (let row = 0; row < tableRows.length; row++) {
const database: string = tableRows[row][2];
if (selectedDatabases.includes(database)) {
selectedRows.push(row);
}
}
await this._databaseSelectorTable.updateProperty('data', tableRows);
this._databaseSelectorTable.selectedRows = selectedRows;
await this.updateValuesOnSelection();
} }
public async createRootContainer(view: azdata.ModelView): Promise<azdata.FlexContainer> { public async createRootContainer(view: azdata.ModelView): Promise<azdata.FlexContainer> {
const providerId = (await this.migrationStateModel.getSourceConnectionProfile()).providerId; await this._loadDatabaseList(this.migrationStateModel, this.migrationStateModel._assessedDatabaseList);
const metaDataService = azdata.dataprotocol.getProvider<azdata.MetadataProvider>(providerId, azdata.DataProviderType.MetadataProvider);
const ownerUri = await azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId);
const results = <azdata.DatabaseInfo[]>await metaDataService.getDatabases(ownerUri);
const excludeDbs: string[] = [
'master',
'tempdb',
'msdb',
'model'
];
this._dbNames = [];
let finalResult = results.filter((db) => !excludeDbs.includes(db.options.name));
finalResult.sort((a, b) => a.options.name.localeCompare(b.options.name));
this._databaseTableValues = [];
for (let index in finalResult) {
let selectable = true;
if (constants.OFFLINE_CAPS.includes(finalResult[index].options.state)) {
selectable = false;
}
this._databaseTableValues.push([
{
value: false,
style: styleCheckBox,
enabled: selectable
},
{
value: this.createIconTextCell(IconPathHelper.sqlDatabaseLogo, finalResult[index].options.name),
style: styleLeft
},
{
value: `${finalResult[index].options.state}`,
style: styleLeft
},
{
value: `${finalResult[index].options.sizeInMB}`,
style: styleRight
},
{
value: `${finalResult[index].options.lastBackup}`,
style: styleLeft
}
]);
this._dbNames.push(finalResult[index].options.name);
}
const text = this._view.modelBuilder.text().withProps({ const text = this._view.modelBuilder.text().withProps({
value: constants.DATABASE_FOR_ASSESSMENT_DESCRIPTION, value: constants.DATABASE_FOR_ASSESSMENT_DESCRIPTION,
@@ -214,70 +148,83 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
}).component(); }).component();
this._dbCount = this._view.modelBuilder.text().withProps({ this._dbCount = this._view.modelBuilder.text().withProps({
value: constants.DATABASES_SELECTED(this.selectedDbs.length, this._databaseTableValues.length), value: constants.DATABASES_SELECTED(
this.selectedDbs().length,
this._databaseTableValues.length),
CSSStyles: { CSSStyles: {
...styles.BODY_CSS, ...styles.BODY_CSS,
'margin-top': '8px' 'margin-top': '8px'
} }
}).component(); }).component();
this._databaseSelectorTable = this._view.modelBuilder.declarativeTable().withProps( const cssClass = 'no-borders';
{ this._databaseSelectorTable = this._view.modelBuilder.table()
enableRowSelection: true, .withProps({
width: '100%', data: [],
CSSStyles: { width: 650,
'border': 'none' height: '100%',
}, forceFitColumns: azdata.ColumnSizingMode.ForceFit,
columns: [ columns: [
{ <azdata.CheckboxColumn>{
displayName: '', value: '',
valueType: azdata.DeclarativeDataType.boolean, width: 10,
width: 20, type: azdata.ColumnType.checkBox,
isReadOnly: false, action: azdata.ActionOnCellCheckboxCheck.selectRow,
showCheckAll: true, resizable: false,
headerCssStyles: styleCheckBox cssClass: cssClass,
headerCssClass: cssClass,
}, },
{ {
displayName: constants.DATABASE, value: 'databaseicon',
// undo when bug #16445 is fixed name: '',
// valueType: azdata.DeclarativeDataType.component, width: 10,
valueType: azdata.DeclarativeDataType.string, type: azdata.ColumnType.icon,
width: '100%', headerCssClass: cssClass,
isReadOnly: true, cssClass: cssClass,
headerCssStyles: styleLeft resizable: false,
}, },
{ {
displayName: constants.STATUS, name: constants.DATABASE,
valueType: azdata.DeclarativeDataType.string, value: 'database',
width: 100, type: azdata.ColumnType.text,
isReadOnly: true, width: 360,
headerCssStyles: styleLeft cssClass: cssClass,
headerCssClass: cssClass,
}, },
{ {
displayName: constants.SIZE, name: constants.STATUS,
valueType: azdata.DeclarativeDataType.string, value: 'status',
width: 125, type: azdata.ColumnType.text,
isReadOnly: true, width: 80,
headerCssStyles: styleRight cssClass: cssClass,
headerCssClass: cssClass,
}, },
{ {
displayName: constants.LAST_BACKUP, name: constants.SIZE,
valueType: azdata.DeclarativeDataType.string, value: 'size',
width: 150, type: azdata.ColumnType.text,
isReadOnly: true, width: 80,
headerCssStyles: styleLeft cssClass: cssClass,
} headerCssClass: cssClass,
},
{
name: constants.LAST_BACKUP,
value: 'lastBackup',
type: azdata.ColumnType.text,
width: 130,
cssClass: cssClass,
headerCssClass: cssClass,
},
] ]
} }).component();
).component();
this._databaseTableValues = selectDatabasesFromList(this.migrationStateModel._databasesForAssessment, this._databaseTableValues); this._disposables.push(this._databaseSelectorTable.onRowSelected(async (e) => {
await this._databaseSelectorTable.setDataValues(this._databaseTableValues);
await this.updateValuesOnSelection();
this._disposables.push(this._databaseSelectorTable.onDataChanged(async () => {
await this.updateValuesOnSelection(); await this.updateValuesOnSelection();
})); }));
// load unfiltered table list and pre-select list of databases saved in state
await this._filterTableList('', this.migrationStateModel._databasesForAssessment);
const flex = view.modelBuilder.flexContainer().withLayout({ const flex = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column', flexFlow: 'column',
height: '100%', height: '100%',
@@ -293,65 +240,60 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
return flex; return flex;
} }
private async _loadDatabaseList(stateMachine: MigrationStateModel, selectedDatabases: string[]): Promise<void> {
const providerId = (await stateMachine.getSourceConnectionProfile()).providerId;
const metaDataService = azdata.dataprotocol.getProvider<azdata.MetadataProvider>(
providerId,
azdata.DataProviderType.MetadataProvider);
const ownerUri = await azdata.connection.getUriForConnection(
stateMachine.sourceConnectionId);
const excludeDbs: string[] = [
'master',
'tempdb',
'msdb',
'model'
];
const databaseList = (<azdata.DatabaseInfo[]>await metaDataService
.getDatabases(ownerUri))
.filter(database => !excludeDbs.includes(database.options.name))
|| [];
databaseList.sort((a, b) => a.options.name.localeCompare(b.options.name));
this._dbNames = [];
this._databaseTableValues = databaseList.map(database => {
const databaseName = database.options.name;
this._dbNames.push(databaseName);
return [
selectedDatabases?.indexOf(databaseName) > -1,
<azdata.IconColumnCellValue>{
icon: IconPathHelper.sqlDatabaseLogo,
title: databaseName,
},
databaseName,
database.options.state,
database.options.sizeInMB,
database.options.lastBackup,
];
}) || [];
}
public selectedDbs(): string[] { public selectedDbs(): string[] {
let result: string[] = []; const rows = this._databaseSelectorTable?.data || [];
this._databaseSelectorTable?.dataValues?.forEach((arr, index) => { const databases = this._databaseSelectorTable?.selectedRows || [];
if (arr[0].value === true) { return databases
result.push(this._dbNames[index]); .filter(row => row < rows.length)
} .map(row => rows[row][2])
}); || [];
return result;
} }
private async updateValuesOnSelection() { private async updateValuesOnSelection() {
const selectedDatabases = this.selectedDbs() || [];
await this._dbCount.updateProperties({ await this._dbCount.updateProperties({
'value': constants.DATABASES_SELECTED(this.selectedDbs().length, this._databaseTableValues.length) 'value': constants.DATABASES_SELECTED(
selectedDatabases.length,
this._databaseSelectorTable.data?.length || 0)
}); });
this.migrationStateModel._databasesForAssessment = this.selectedDbs(); this.migrationStateModel._databasesForAssessment = selectedDatabases;
} }
// undo when bug #16445 is fixed
private createIconTextCell(icon: IconPath, text: string): string {
return text;
}
// private createIconTextCell(icon: IconPath, text: string): azdata.FlexContainer {
// const cellContainer = this._view.modelBuilder.flexContainer().withProps({
// CSSStyles: {
// 'justify-content': 'left'
// }
// }).component();
// const iconComponent = this._view.modelBuilder.image().withProps({
// iconPath: icon,
// iconWidth: '16px',
// iconHeight: '16px',
// width: '20px',
// height: '20px'
// }).component();
// cellContainer.addItem(iconComponent, {
// flex: '0',
// CSSStyles: {
// 'width': '32px'
// }
// });
// const textComponent = this._view.modelBuilder.text().withProps({
// value: text,
// title: text,
// CSSStyles: {
// 'margin': '0px',
// 'width': '110px'
// }
// }).component();
// cellContainer.addItem(textComponent, {
// CSSStyles: {
// 'width': 'auto'
// }
// });
// return cellContainer;
// }
// undo when bug #16445 is fixed
} }

View File

@@ -44,27 +44,21 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
protected async registerContent(view: azdata.ModelView): Promise<void> { protected async registerContent(view: azdata.ModelView): Promise<void> {
this._view = view; this._view = view;
this._statusLoadingComponent = view.modelBuilder.loadingComponent().withItem(this.createDMSDetailsContainer()).component(); this._statusLoadingComponent = view.modelBuilder.loadingComponent()
.withItem(this.createDMSDetailsContainer())
.component();
this._dmsInfoContainer = this._view.modelBuilder.flexContainer().withItems([ this._dmsInfoContainer = this._view.modelBuilder.flexContainer()
this._statusLoadingComponent .withItems([this._statusLoadingComponent])
]).component(); .component();
const form = view.modelBuilder.formContainer() const form = view.modelBuilder.formContainer()
.withFormItems( .withFormItems([
[ { component: this.migrationServiceDropdownContainer() },
{ { component: this._dmsInfoContainer }
component: this.migrationServiceDropdownContainer() ])
}, .withProps({ CSSStyles: { 'padding-top': '0' } })
{ .component();
component: this._dmsInfoContainer
}
]
).withProps({
CSSStyles: {
'padding-top': '0'
}
}).component();
this._disposables.push(this._view.onClosed(e => { this._disposables.push(this._view.onClosed(e => {
this._disposables.forEach( this._disposables.forEach(
@@ -419,29 +413,26 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
this.migrationStateModel._targetSubscription, this.migrationStateModel._targetSubscription,
this.migrationStateModel._sqlMigrationService.properties.resourceGroup, this.migrationStateModel._sqlMigrationService.properties.resourceGroup,
this.migrationStateModel._sqlMigrationService.location, this.migrationStateModel._sqlMigrationService.location,
this.migrationStateModel._sqlMigrationService.name, this.migrationStateModel._sqlMigrationService.name);
this.migrationStateModel._sessionId);
this.migrationStateModel._sqlMigrationService = migrationService; this.migrationStateModel._sqlMigrationService = migrationService;
const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData( const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData(
this.migrationStateModel._azureAccount, this.migrationStateModel._azureAccount,
this.migrationStateModel._targetSubscription, this.migrationStateModel._targetSubscription,
this.migrationStateModel._sqlMigrationService.properties.resourceGroup, this.migrationStateModel._sqlMigrationService.properties.resourceGroup,
this.migrationStateModel._sqlMigrationService.location, this.migrationStateModel._sqlMigrationService.location,
this.migrationStateModel._sqlMigrationService!.name, this.migrationStateModel._sqlMigrationService!.name);
this.migrationStateModel._sessionId); this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map(
this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map(node => node.nodeName); node => node.nodeName);
const migrationServiceAuthKeys = await getSqlMigrationServiceAuthKeys( const migrationServiceAuthKeys = await getSqlMigrationServiceAuthKeys(
this.migrationStateModel._azureAccount, this.migrationStateModel._azureAccount,
this.migrationStateModel._targetSubscription, this.migrationStateModel._targetSubscription,
this.migrationStateModel._sqlMigrationService.properties.resourceGroup, this.migrationStateModel._sqlMigrationService.properties.resourceGroup,
this.migrationStateModel._sqlMigrationService.location, this.migrationStateModel._sqlMigrationService.location,
this.migrationStateModel._sqlMigrationService!.name, this.migrationStateModel._sqlMigrationService!.name);
this.migrationStateModel._sessionId
);
this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map((node) => { this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map(
return node.nodeName; node => node.nodeName);
});
const state = migrationService.properties.integrationRuntimeState; const state = migrationService.properties.integrationRuntimeState;
if (state === 'Online') { if (state === 'Online') {
@@ -458,25 +449,21 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
const data = [ const data = [
[ [
{ value: constants.SERVICE_KEY1_LABEL },
{ value: migrationServiceAuthKeys.authKey1 },
{ {
value: constants.SERVICE_KEY1_LABEL value: this._view.modelBuilder.flexContainer()
}, .withItems([this._copy1, this._refresh1])
{ .component()
value: migrationServiceAuthKeys.authKey1
},
{
value: this._view.modelBuilder.flexContainer().withItems([this._copy1, this._refresh1]).component()
} }
], ],
[ [
{ value: constants.SERVICE_KEY2_LABEL },
{ value: migrationServiceAuthKeys.authKey2 },
{ {
value: constants.SERVICE_KEY2_LABEL value: this._view.modelBuilder.flexContainer()
}, .withItems([this._copy2, this._refresh2])
{ .component()
value: migrationServiceAuthKeys.authKey2
},
{
value: this._view.modelBuilder.flexContainer().withItems([this._copy2, this._refresh2]).component()
} }
] ]
]; ];

View File

@@ -186,7 +186,8 @@ export class TargetSelectionPage extends MigrationWizardPage {
const selectedAzureAccount = this.migrationStateModel.getAccount(selectedIndex); const selectedAzureAccount = this.migrationStateModel.getAccount(selectedIndex);
// Making a clone of the account object to preserve the original tenants // Making a clone of the account object to preserve the original tenants
this.migrationStateModel._azureAccount = deepClone(selectedAzureAccount); this.migrationStateModel._azureAccount = deepClone(selectedAzureAccount);
if (this.migrationStateModel._azureAccount.properties.tenants.length > 1) { if (selectedAzureAccount.isStale === false &&
this.migrationStateModel._azureAccount.properties.tenants.length > 1) {
this.migrationStateModel._accountTenants = selectedAzureAccount.properties.tenants; this.migrationStateModel._accountTenants = selectedAzureAccount.properties.tenants;
this._accountTenantDropdown.values = await this.migrationStateModel.getTenantValues(); this._accountTenantDropdown.values = await this.migrationStateModel.getTenantValues();
selectDropDownIndex(this._accountTenantDropdown, 0); selectDropDownIndex(this._accountTenantDropdown, 0);

View File

@@ -17,14 +17,17 @@ import { MigrationModePage } from './migrationModePage';
import { DatabaseSelectorPage } from './databaseSelectorPage'; import { DatabaseSelectorPage } from './databaseSelectorPage';
import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery'; import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery';
import * as styles from '../constants/styles'; import * as styles from '../constants/styles';
import { MigrationLocalStorage, MigrationServiceContext } from '../models/migrationLocalStorage';
import { azureResource } from 'azureResource';
export const WIZARD_INPUT_COMPONENT_WIDTH = '600px'; export const WIZARD_INPUT_COMPONENT_WIDTH = '600px';
export class WizardController { export class WizardController {
private _wizardObject!: azdata.window.Wizard; private _wizardObject!: azdata.window.Wizard;
private _model!: MigrationStateModel;
private _disposables: vscode.Disposable[] = []; private _disposables: vscode.Disposable[] = [];
constructor(private readonly extensionContext: vscode.ExtensionContext, model: MigrationStateModel) { constructor(
this._model = model; private readonly extensionContext: vscode.ExtensionContext,
private readonly _model: MigrationStateModel,
private readonly _onClosedCallback: () => Promise<void>) {
} }
public async openWizard(connectionId: string): Promise<void> { public async openWizard(connectionId: string): Promise<void> {
@@ -105,13 +108,12 @@ export class WizardController {
}); });
await Promise.all(wizardSetupPromises); await Promise.all(wizardSetupPromises);
this._model.extensionContext.subscriptions.push(this._wizardObject.onPageChanged(async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => { this._model.extensionContext.subscriptions.push(
await pages[0].onPageEnter(pageChangeInfo); this._wizardObject.onPageChanged(
})); async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => {
await pages[0].onPageEnter(pageChangeInfo);
}));
this._model.extensionContext.subscriptions.push(this._wizardObject.doneButton.onClick(async (e) => {
await stateModel.startMigration();
}));
this._disposables.push(saveAndCloseButton.onClick(async () => { this._disposables.push(saveAndCloseButton.onClick(async () => {
await stateModel.saveInfo(serverName, this._wizardObject.currentPage); await stateModel.saveInfo(serverName, this._wizardObject.currentPage);
await this._wizardObject.close(); await this._wizardObject.close();
@@ -134,16 +136,65 @@ export class WizardController {
this._wizardObject.doneButton.label = loc.START_MIGRATION_TEXT; this._wizardObject.doneButton.label = loc.START_MIGRATION_TEXT;
this._disposables.push(this._wizardObject.doneButton.onClick(e => { this._disposables.push(
sendSqlMigrationActionEvent( this._wizardObject.doneButton.onClick(async (e) => {
TelemetryViews.SqlMigrationWizard, await stateModel.startMigration();
TelemetryAction.PageButtonClick, await this.updateServiceContext(stateModel);
{ await this._onClosedCallback();
...this.getTelemetryProps(),
'buttonPressed': TelemetryAction.Done, sendSqlMigrationActionEvent(
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title TelemetryViews.SqlMigrationWizard,
}, {}); TelemetryAction.PageButtonClick,
})); {
...this.getTelemetryProps(),
'buttonPressed': TelemetryAction.Done,
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
}, {});
}));
}
private async updateServiceContext(stateModel: MigrationStateModel): Promise<void> {
const resourceGroup = this._getResourceGroupByName(
stateModel._resourceGroups,
stateModel._sqlMigrationService?.properties.resourceGroup);
const subscription = this._getSubscriptionFromResourceId(
stateModel._subscriptions,
resourceGroup?.id);
const location = this._getLocationByValue(
stateModel._locations,
stateModel._sqlMigrationService?.location);
return await MigrationLocalStorage.saveMigrationServiceContext(
<MigrationServiceContext>{
azureAccount: stateModel._azureAccount,
tenant: stateModel._azureTenant,
subscription: subscription,
location: location,
resourceGroup: resourceGroup,
migrationService: stateModel._sqlMigrationService,
});
}
private _getResourceGroupByName(resourceGroups: azureResource.AzureResourceResourceGroup[], displayName?: string): azureResource.AzureResourceResourceGroup | undefined {
return resourceGroups.find(rg => rg.name === displayName);
}
private _getLocationByValue(locations: azureResource.AzureLocation[], name?: string): azureResource.AzureLocation | undefined {
return locations.find(loc => loc.name === name);
}
private _getSubscriptionFromResourceId(subscriptions: azureResource.AzureResourceSubscription[], resourceId?: string): azureResource.AzureResourceSubscription | undefined {
let parts = resourceId?.split('/subscriptions/');
if (parts?.length && parts?.length > 1) {
parts = parts[1]?.split('/resourcegroups/');
if (parts?.length && parts?.length > 0) {
const subscriptionId: string = parts[0];
return subscriptions.find(sub => sub.id === subscriptionId, 1);
}
}
return undefined;
} }
private async sendPageButtonClickEvent(pageChangeInfo: azdata.window.WizardPageChangeInfo) { private async sendPageButtonClickEvent(pageChangeInfo: azdata.window.WizardPageChangeInfo) {