mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
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:
@@ -1,5 +1,5 @@
|
||||
# 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.
|
||||
- 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.
|
||||
|
||||
32
extensions/sql-migration/images/sqlMigrationService.svg
Normal file
32
extensions/sql-migration/images/sqlMigrationService.svg
Normal 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 |
3
extensions/sql-migration/images/view.svg
Normal file
3
extensions/sql-migration/images/view.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path 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 |
@@ -2,9 +2,9 @@
|
||||
"name": "sql-migration",
|
||||
"displayName": "%displayName%",
|
||||
"description": "%description%",
|
||||
"version": "0.1.14",
|
||||
"version": "1.0.0",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"preview": false,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
"icon": "images/extension.png",
|
||||
"aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"start-migration-command": "Migrate to Azure SQL",
|
||||
"send-feedback-command": "Feedback",
|
||||
"new-support-request-command": "New support request",
|
||||
"refresh-migrations-command": "Refresh",
|
||||
"migration-context-menu-category": "Migration Context Menu",
|
||||
"complete-cutover-menu": "Complete cutover",
|
||||
"database-details-menu": "Database details",
|
||||
|
||||
@@ -142,10 +142,15 @@ export async function getBlobs(account: azdata.Account, subscription: Subscripti
|
||||
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 path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}?api-version=2020-09-01-preview`);
|
||||
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId));
|
||||
const path = encodeURI(`${sqlMigrationServiceId}?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());
|
||||
}
|
||||
@@ -153,6 +158,20 @@ export async function getSqlMigrationService(account: azdata.Account, subscripti
|
||||
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[]> {
|
||||
const api = await getAzureCoreAPI();
|
||||
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> {
|
||||
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 = {
|
||||
'location': regionName
|
||||
};
|
||||
@@ -181,7 +200,7 @@ export async function createSqlMigrationService(account: azdata.Account, subscri
|
||||
const maxRetry = 24;
|
||||
let i = 0;
|
||||
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;
|
||||
if (creationStatus === ProvisioningState.Succeeded) {
|
||||
break;
|
||||
@@ -196,10 +215,10 @@ export async function createSqlMigrationService(account: azdata.Account, subscri
|
||||
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 path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}/ListAuthKeys?api-version=2020-09-01-preview`);
|
||||
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, getSessionIdHeader(sessionId));
|
||||
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);
|
||||
if (response.errors.length > 0) {
|
||||
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 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 = {
|
||||
'location': regionName,
|
||||
'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) {
|
||||
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 path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationService}/monitoringData?api-version=2020-09-01-preview`);
|
||||
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId));
|
||||
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);
|
||||
if (response.errors.length > 0) {
|
||||
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> {
|
||||
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));
|
||||
if (response.errors.length > 0) {
|
||||
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> {
|
||||
if (!migration.id) {
|
||||
throw new Error('NullMigrationId');
|
||||
}
|
||||
|
||||
const migrationOperationId = migration.properties?.migrationOperationId;
|
||||
if (migrationOperationId === undefined &&
|
||||
migration.properties.provisioningState === ProvisioningState.Failed) {
|
||||
return migration;
|
||||
}
|
||||
export async function getMigrationDetails(account: azdata.Account, subscription: Subscription, migrationId: string, migrationOperationId?: string): Promise<DatabaseMigration> {
|
||||
|
||||
const path = migrationOperationId === undefined
|
||||
? encodeURI(`${migration.id}?$expand=MigrationStatusDetails&api-version=2020-09-01-preview`)
|
||||
: encodeURI(`${migration.id}?migrationOperationId=${migrationOperationId}&$expand=MigrationStatusDetails&api-version=2020-09-01-preview`);
|
||||
? encodeURI(`${migrationId}?$expand=MigrationStatusDetails&api-version=2022-01-30-preview`)
|
||||
: encodeURI(`${migrationId}?migrationOperationId=${migrationOperationId}&$expand=MigrationStatusDetails&api-version=2022-01-30-preview`);
|
||||
|
||||
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) {
|
||||
throw new Error(response.errors.toString());
|
||||
}
|
||||
|
||||
const migrationUpdate: DatabaseMigration = 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;
|
||||
return response.response.data;
|
||||
}
|
||||
|
||||
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 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) {
|
||||
throw new Error(response.errors.toString());
|
||||
}
|
||||
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 path = encodeURI(`${sqlMigrationService.id}/listMigrations?$expand=MigrationStatusDetails&api-version=2020-09-01-preview`);
|
||||
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId));
|
||||
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.POST, undefined, true, undefined, undefined);
|
||||
if (response.errors.length > 0) {
|
||||
throw new Error(response.errors.toString());
|
||||
}
|
||||
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 path = encodeURI(`${migrationStatus.id}/operations/${migrationStatus.properties.migrationOperationId}/cutover?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) {
|
||||
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));
|
||||
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, undefined);
|
||||
if (response.errors.length > 0) {
|
||||
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 {
|
||||
return id.replace(RegExp('^(.*?)/resourceGroups/'), '').replace(RegExp('/providers/.*'), '').toLowerCase();
|
||||
}
|
||||
@@ -452,6 +484,7 @@ export interface DatabaseMigration {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface DatabaseMigrationProperties {
|
||||
scope: string;
|
||||
provisioningState: 'Succeeded' | 'Failed' | 'Creating';
|
||||
@@ -460,8 +493,8 @@ export interface DatabaseMigrationProperties {
|
||||
migrationStatusDetails?: MigrationStatusDetails;
|
||||
startedOn: string;
|
||||
endedOn: string;
|
||||
sourceSqlConnection: SqlConnectionInfo;
|
||||
sourceDatabaseName: string;
|
||||
sourceServerName: string;
|
||||
targetDatabaseCollation: string;
|
||||
migrationService: string;
|
||||
migrationOperationId: string;
|
||||
@@ -469,6 +502,7 @@ export interface DatabaseMigrationProperties {
|
||||
offlineConfiguration: OfflineConfiguration;
|
||||
migrationFailureError: ErrorInfo;
|
||||
}
|
||||
|
||||
export interface MigrationStatusDetails {
|
||||
migrationState: string;
|
||||
startedOn: string;
|
||||
@@ -528,6 +562,7 @@ export interface BackupSetInfo {
|
||||
export interface SourceLocation {
|
||||
fileShare?: DatabaseMigrationFileShare;
|
||||
azureBlob?: DatabaseMigrationAzureBlob;
|
||||
fileStorageType: 'FileShare' | 'AzureBlob' | 'None';
|
||||
}
|
||||
|
||||
export interface TargetLocation {
|
||||
|
||||
@@ -7,8 +7,9 @@ import { window, CategoryValue, DropDownComponent, IconPath } from 'azdata';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { DAYS, HRS, MINUTE, SEC } from '../constants/strings';
|
||||
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 { DatabaseMigration } from './azure';
|
||||
|
||||
export function deepClone<T>(obj: T): T {
|
||||
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[] {
|
||||
let filteredMigration: MigrationContext[] = [];
|
||||
export function filterMigrations(databaseMigrations: DatabaseMigration[], statusFilter: string, databaseNameFilter?: string): DatabaseMigration[] {
|
||||
let filteredMigration: DatabaseMigration[] = [];
|
||||
if (statusFilter === AdsMigrationStatus.ALL) {
|
||||
filteredMigration = databaseMigrations;
|
||||
} else if (statusFilter === AdsMigrationStatus.ONGOING) {
|
||||
filteredMigration = databaseMigrations.filter((value) => {
|
||||
const status = value.migrationContext.properties?.migrationStatus;
|
||||
const provisioning = value.migrationContext.properties?.provisioningState;
|
||||
return status === MigrationStatus.InProgress
|
||||
|| status === MigrationStatus.Creating
|
||||
|| provisioning === MigrationStatus.Creating;
|
||||
});
|
||||
filteredMigration = databaseMigrations.filter(
|
||||
value => {
|
||||
const status = value.properties?.migrationStatus;
|
||||
return status === MigrationStatus.InProgress
|
||||
|| status === MigrationStatus.Creating
|
||||
|| value.properties?.provisioningState === MigrationStatus.Creating;
|
||||
});
|
||||
} else if (statusFilter === AdsMigrationStatus.SUCCEEDED) {
|
||||
filteredMigration = databaseMigrations.filter((value) => {
|
||||
const status = value.migrationContext.properties?.migrationStatus;
|
||||
return status === MigrationStatus.Succeeded;
|
||||
});
|
||||
filteredMigration = databaseMigrations.filter(
|
||||
value => value.properties?.migrationStatus === MigrationStatus.Succeeded);
|
||||
} else if (statusFilter === AdsMigrationStatus.FAILED) {
|
||||
filteredMigration = databaseMigrations.filter((value) => {
|
||||
const status = value.migrationContext.properties?.migrationStatus;
|
||||
const provisioning = value.migrationContext.properties?.provisioningState;
|
||||
return status === MigrationStatus.Failed
|
||||
|| provisioning === ProvisioningState.Failed;
|
||||
});
|
||||
filteredMigration = databaseMigrations.filter(
|
||||
value =>
|
||||
value.properties?.migrationStatus === MigrationStatus.Failed ||
|
||||
value.properties?.provisioningState === ProvisioningState.Failed);
|
||||
} else if (statusFilter === AdsMigrationStatus.COMPLETING) {
|
||||
filteredMigration = databaseMigrations.filter((value) => {
|
||||
const status = value.migrationContext.properties?.migrationStatus;
|
||||
return status === MigrationStatus.Completing;
|
||||
});
|
||||
filteredMigration = databaseMigrations.filter(
|
||||
value => value.properties?.migrationStatus === MigrationStatus.Completing);
|
||||
}
|
||||
if (databaseNameFilter) {
|
||||
filteredMigration = filteredMigration.filter((value) => {
|
||||
return value.migrationContext.name.toLowerCase().includes(databaseNameFilter.toLowerCase());
|
||||
});
|
||||
const filter = databaseNameFilter.toLowerCase();
|
||||
filteredMigration = filteredMigration.filter(
|
||||
migration => migration.name?.toLowerCase().includes(filter));
|
||||
}
|
||||
return filteredMigration;
|
||||
}
|
||||
@@ -144,35 +139,36 @@ export function convertIsoTimeToLocalTime(isoTime: string): Date {
|
||||
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 {
|
||||
const selectedIndex = value ? findDropDownItemIndex(dropDown, value, useDisplayName) : -1;
|
||||
if (selectedIndex > -1) {
|
||||
selectDropDownIndex(dropDown, selectedIndex);
|
||||
} else {
|
||||
selectDropDownIndex(dropDown, 0);
|
||||
if (dropDown.values && dropDown.values.length > 0) {
|
||||
const selectedIndex = value ? findDropDownItemIndex(dropDown, value, useDisplayName) : -1;
|
||||
if (selectedIndex > -1) {
|
||||
selectDropDownIndex(dropDown, selectedIndex);
|
||||
} else {
|
||||
selectDropDownIndex(dropDown, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function selectDropDownIndex(dropDown: DropDownComponent, index: number): void {
|
||||
if (index >= 0 && dropDown.values && index <= dropDown.values.length - 1) {
|
||||
const value = dropDown.values[index];
|
||||
dropDown.value = value as CategoryValue;
|
||||
if (dropDown.values && dropDown.values.length > 0) {
|
||||
if (index >= 0 && index <= dropDown.values.length - 1) {
|
||||
dropDown.value = dropDown.values[index] as CategoryValue;
|
||||
return;
|
||||
}
|
||||
}
|
||||
dropDown.value = undefined;
|
||||
}
|
||||
|
||||
export function findDropDownItemIndex(dropDown: DropDownComponent, value: string, useDisplayName: boolean = true): number {
|
||||
if (dropDown.values) {
|
||||
if (useDisplayName) {
|
||||
return dropDown.values.findIndex((v: any) =>
|
||||
(v as CategoryValue)?.displayName?.toLowerCase() === value?.toLowerCase());
|
||||
} else {
|
||||
return dropDown.values.findIndex((v: any) =>
|
||||
(v as CategoryValue)?.name?.toLowerCase() === value?.toLowerCase());
|
||||
}
|
||||
if (value && dropDown.values && dropDown.values.length > 0) {
|
||||
const searachValue = value?.toLowerCase();
|
||||
return useDisplayName
|
||||
? dropDown.values.findIndex((v: any) =>
|
||||
(v as CategoryValue)?.displayName?.toLowerCase() === searachValue)
|
||||
: dropDown.values.findIndex((v: any) =>
|
||||
(v as CategoryValue)?.name?.toLowerCase() === searachValue);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,45 +4,70 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { MigrationContext, MigrationStatus } from '../models/migrationLocalStorage';
|
||||
import { MigrationMode, MigrationTargetType } from '../models/stateMachine';
|
||||
import { DatabaseMigration } from '../api/azure';
|
||||
import { MigrationStatus } from '../models/migrationLocalStorage';
|
||||
import { FileStorageType, MigrationMode, MigrationTargetType } from '../models/stateMachine';
|
||||
import * as loc from './strings';
|
||||
|
||||
export enum SQLTargetAssetType {
|
||||
SQLMI = 'microsoft.sql/managedinstances',
|
||||
SQLVM = 'Microsoft.SqlVirtualMachine/sqlVirtualMachines',
|
||||
SQLDB = 'Microsoft.Sql/servers',
|
||||
}
|
||||
|
||||
export function getMigrationTargetType(migration: MigrationContext): string {
|
||||
switch (migration.targetManagedInstance.type) {
|
||||
case SQLTargetAssetType.SQLMI:
|
||||
return loc.SQL_MANAGED_INSTANCE;
|
||||
case SQLTargetAssetType.SQLVM:
|
||||
return loc.SQL_VIRTUAL_MACHINE;
|
||||
default:
|
||||
return '';
|
||||
export function getMigrationTargetType(migration: DatabaseMigration): string {
|
||||
const id = migration.id?.toLowerCase();
|
||||
if (id?.indexOf(SQLTargetAssetType.SQLMI.toLowerCase()) > -1) {
|
||||
return loc.SQL_MANAGED_INSTANCE;
|
||||
}
|
||||
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 {
|
||||
switch (migration.targetManagedInstance.type) {
|
||||
export function getMigrationTargetTypeEnum(migration: DatabaseMigration): MigrationTargetType | undefined {
|
||||
switch (migration.type) {
|
||||
case SQLTargetAssetType.SQLMI:
|
||||
return MigrationTargetType.SQLMI;
|
||||
case SQLTargetAssetType.SQLVM:
|
||||
return MigrationTargetType.SQLVM;
|
||||
case SQLTargetAssetType.SQLDB:
|
||||
return MigrationTargetType.SQLDB;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getMigrationMode(migration: MigrationContext): string {
|
||||
return migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? loc.OFFLINE : loc.ONLINE;
|
||||
export function getMigrationMode(migration: DatabaseMigration): string {
|
||||
return isOfflineMigation(migration)
|
||||
? loc.OFFLINE
|
||||
: loc.ONLINE;
|
||||
}
|
||||
|
||||
export function getMigrationModeEnum(migration: MigrationContext): MigrationMode {
|
||||
return migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? MigrationMode.OFFLINE : MigrationMode.ONLINE;
|
||||
export function getMigrationModeEnum(migration: DatabaseMigration): MigrationMode {
|
||||
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 {
|
||||
return status === undefined ||
|
||||
status === MigrationStatus.Failed ||
|
||||
@@ -50,15 +75,14 @@ export function canRetryMigration(status: string | undefined): boolean {
|
||||
status === MigrationStatus.Canceled;
|
||||
}
|
||||
|
||||
|
||||
const TABLE_CHECKBOX_INDEX = 0;
|
||||
const TABLE_DB_NAME_INDEX = 1;
|
||||
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()) || [];
|
||||
if (sourceDatabaseNames?.length > 0) {
|
||||
for (let i in databaseTableValues) {
|
||||
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) {
|
||||
row[TABLE_CHECKBOX_INDEX].value = true;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ export class IconPathHelper {
|
||||
public static edit: IconPath;
|
||||
public static restartDataCollection: IconPath;
|
||||
public static stop: IconPath;
|
||||
public static view: IconPath;
|
||||
public static sqlMigrationService: IconPath;
|
||||
|
||||
public static setExtensionContext(context: vscode.ExtensionContext) {
|
||||
IconPathHelper.copy = {
|
||||
@@ -173,5 +175,13 @@ export class IconPathHelper {
|
||||
light: 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'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 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_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");
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
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
|
||||
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.");
|
||||
@@ -453,7 +471,7 @@ export const CREATE = localize('sql.migration.create', "Create");
|
||||
export const CANCEL = localize('sql.migration.cancel', "Cancel");
|
||||
export const TYPE = localize('sql.migration.type', "Type");
|
||||
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 AZURE_SQL = localize('sql.migration.azure.sql', "Azure SQL");
|
||||
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.");
|
||||
|
||||
// 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_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");
|
||||
@@ -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_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_FAILED = localize('sql.migration.failed', "Migrations failed");
|
||||
export const MIGRATION_COMPLETED = localize('sql.migration.migration.completed', "Migrations completed");
|
||||
export const MIGRATION_CUTOVER_CARD = localize('sql.migration.cutover.card', "Completing cutover");
|
||||
export const MIGRATION_NOT_STARTED = localize('sql.migration.migration.not.started', "Migrations not started");
|
||||
export const MIGRATION_FAILED = localize('sql.migration.failed', "Database migrations failed");
|
||||
export const MIGRATION_COMPLETED = localize('sql.migration.migration.completed', "Database migrations completed");
|
||||
export const MIGRATION_CUTOVER_CARD = localize('sql.migration.cutover.card', "Database migrations completing cutover");
|
||||
export const SHOW_STATUS = localize('sql.migration.show.status', "Show status");
|
||||
export function MIGRATION_INPROGRESS_WARNING(count: number) {
|
||||
switch (count) {
|
||||
@@ -593,6 +613,7 @@ export const NO_PENDING_BACKUPS = localize('sql.migration.no.pending.backups', "
|
||||
//Migration status dialog
|
||||
export const ADD_ACCOUNT = localize('sql.migration.status.add.account', "Add account");
|
||||
export const ADD_ACCOUNT_MESSAGE = localize('sql.migration.status.add.account.MESSAGE', "Add your Azure account to view existing migrations and their status.");
|
||||
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_ONGOING = localize('sql.migration.status.dropdown.ongoing', "Status: Ongoing");
|
||||
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 OFFLINE = localize('sql.migration.offline', "Offline");
|
||||
export const DATABASE = localize('sql.migration.database', "Database");
|
||||
export const STATUS_COLUMN = localize('sql.migration.database.status.column', "Status");
|
||||
export const DATABASE_MIGRATION_SERVICE = localize('sql.migration.database.migration.service', "Database Migration Service");
|
||||
export const DURATION = localize('sql.migration.duration', "Duration");
|
||||
export const AZURE_SQL_TARGET = localize('sql.migration.azure.sql.target', "Target type");
|
||||
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_DATABASE = localize('sql.migration.sql.database', "SQL Database");
|
||||
export const TARGET_AZURE_SQL_INSTANCE_NAME = localize('sql.migration.target.azure.sql.instance.name', "Target name");
|
||||
export const MIGRATION_MODE = localize('sql.migration.cutover.type', "Migration mode");
|
||||
export const 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 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');
|
||||
|
||||
@@ -5,15 +5,17 @@
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { MigrationContext, MigrationLocalStorage } from '../models/migrationLocalStorage';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import * as loc from '../constants/strings';
|
||||
import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { MigrationStatusDialog } from '../dialog/migrationStatus/migrationStatusDialog';
|
||||
import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel';
|
||||
import { filterMigrations, SupportedAutoRefreshIntervals } from '../api/utils';
|
||||
import { filterMigrations } from '../api/utils';
|
||||
import * as styles from '../constants/styles';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
|
||||
import { DatabaseMigration } from '../api/azure';
|
||||
import { getCurrentMigrations, getSelectedServiceStatus, isServiceContextValid, MigrationLocalStorage } from '../models/migrationLocalStorage';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
interface IActionMetadata {
|
||||
@@ -25,7 +27,12 @@ interface IActionMetadata {
|
||||
}
|
||||
|
||||
const maxWidth = 800;
|
||||
const refreshFrequency: SupportedAutoRefreshIntervals = 180000;
|
||||
const BUTTON_CSS = {
|
||||
'font-size': '13px',
|
||||
'line-height': '18px',
|
||||
'margin': '4px 0',
|
||||
'text-align': 'left',
|
||||
};
|
||||
|
||||
interface StatusCard {
|
||||
container: azdata.DivContainer;
|
||||
@@ -37,39 +44,33 @@ interface StatusCard {
|
||||
|
||||
export class DashboardWidget {
|
||||
private _context: vscode.ExtensionContext;
|
||||
|
||||
private _migrationStatusCardsContainer!: azdata.FlexContainer;
|
||||
private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent;
|
||||
private _view!: azdata.ModelView;
|
||||
|
||||
private _inProgressMigrationButton!: StatusCard;
|
||||
private _inProgressWarningMigrationButton!: StatusCard;
|
||||
private _allMigrationButton!: StatusCard;
|
||||
private _successfulMigrationButton!: StatusCard;
|
||||
private _failedMigrationButton!: StatusCard;
|
||||
private _completingMigrationButton!: StatusCard;
|
||||
private _notStartedMigrationCard!: StatusCard;
|
||||
private _migrationStatusMap: Map<string, MigrationContext[]> = new Map();
|
||||
private _viewAllMigrationsButton!: azdata.ButtonComponent;
|
||||
private _selectServiceText!: azdata.TextComponent;
|
||||
private _serviceContextButton!: azdata.ButtonComponent;
|
||||
private _refreshButton!: azdata.ButtonComponent;
|
||||
|
||||
private _autoRefreshHandle!: NodeJS.Timeout;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
private isRefreshing: boolean = false;
|
||||
|
||||
public onDialogClosed = async (): Promise<void> => {
|
||||
const label = await getSelectedServiceStatus();
|
||||
this._serviceContextButton.label = label;
|
||||
this._serviceContextButton.title = label;
|
||||
await this.refreshMigrations();
|
||||
};
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
this._context = context;
|
||||
}
|
||||
|
||||
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 {
|
||||
azdata.ui.registerModelViewProvider('migration.dashboard', async (view) => {
|
||||
this._view = view;
|
||||
@@ -82,7 +83,10 @@ export class DashboardWidget {
|
||||
|
||||
const header = this.createHeader(view);
|
||||
// Files need to have the vscode-file scheme to be loaded by ADS
|
||||
const watermarkUri = vscode.Uri.file(<string>IconPathHelper.migrationDashboardHeaderBackground.light).with({ scheme: 'vscode-file' });
|
||||
const watermarkUri = vscode.Uri
|
||||
.file(<string>IconPathHelper.migrationDashboardHeaderBackground.light)
|
||||
.with({ scheme: 'vscode-file' });
|
||||
|
||||
container.addItem(header, {
|
||||
CSSStyles: {
|
||||
'background-image': `
|
||||
@@ -107,11 +111,11 @@ export class DashboardWidget {
|
||||
'margin': '0 24px'
|
||||
}
|
||||
});
|
||||
this._disposables.push(this._view.onClosed(e => {
|
||||
clearInterval(this._autoRefreshHandle);
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
this._disposables.push(
|
||||
this._view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
|
||||
await view.initializeModel(container);
|
||||
await this.refreshMigrations();
|
||||
@@ -119,8 +123,6 @@ export class DashboardWidget {
|
||||
}
|
||||
|
||||
private createHeader(view: azdata.ModelView): azdata.FlexContainer {
|
||||
this.setAutoRefresh(refreshFrequency);
|
||||
|
||||
const header = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column',
|
||||
width: maxWidth,
|
||||
@@ -229,95 +231,71 @@ export class DashboardWidget {
|
||||
'transition': 'all .5s ease',
|
||||
}
|
||||
}).component();
|
||||
this._disposables.push(buttonContainer.onDidClick(async () => {
|
||||
if (taskMetaData.command) {
|
||||
await vscode.commands.executeCommand(taskMetaData.command);
|
||||
}
|
||||
}));
|
||||
this._disposables.push(
|
||||
buttonContainer.onDidClick(async () => {
|
||||
if (taskMetaData.command) {
|
||||
await vscode.commands.executeCommand(taskMetaData.command);
|
||||
}
|
||||
}));
|
||||
return view.modelBuilder.divContainer().withItems([buttonContainer]).component();
|
||||
}
|
||||
|
||||
private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void {
|
||||
const classVariable = this;
|
||||
clearInterval(this._autoRefreshHandle);
|
||||
if (interval !== -1) {
|
||||
this._autoRefreshHandle = setInterval(async function () { await classVariable.refreshMigrations(); }, interval);
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshMigrations(): Promise<void> {
|
||||
public async refreshMigrations(): Promise<void> {
|
||||
if (this.isRefreshing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this._viewAllMigrationsButton.enabled = false;
|
||||
this._migrationStatusCardLoadingContainer.loading = true;
|
||||
let migrations: DatabaseMigration[] = [];
|
||||
try {
|
||||
await this.setCurrentMigrations(await this.getMigrations());
|
||||
const migrations = await this.getCurrentMigrations();
|
||||
const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
|
||||
let warningCount = 0;
|
||||
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;
|
||||
migrations = await getCurrentMigrations();
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', e);
|
||||
void vscode.window.showErrorMessage(loc.DASHBOARD_REFRESH_MIGRATIONS(e.message));
|
||||
}
|
||||
|
||||
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[]> {
|
||||
const currentConnection = (await azdata.connection.getCurrentConnection());
|
||||
return await MigrationLocalStorage.getMigrationsBySourceConnections(currentConnection, true);
|
||||
private _updateStatusCard(
|
||||
migrations: DatabaseMigration[],
|
||||
card: StatusCard,
|
||||
status: AdsMigrationStatus,
|
||||
show?: boolean): void {
|
||||
const list = filterMigrations(migrations, status);
|
||||
const count = list?.length || 0;
|
||||
card.container.display = count > 0 || show ? '' : 'none';
|
||||
card.count.value = count.toString();
|
||||
}
|
||||
|
||||
private createStatusCard(
|
||||
cardIconPath: IconPath,
|
||||
cardTitle: string,
|
||||
@@ -334,26 +312,27 @@ export class DashboardWidget {
|
||||
}
|
||||
}).component();
|
||||
|
||||
const statusIcon = this._view.modelBuilder.image().withProps({
|
||||
iconPath: cardIconPath!.light,
|
||||
iconHeight: 24,
|
||||
iconWidth: 24,
|
||||
height: 32,
|
||||
CSSStyles: {
|
||||
'margin': '0 8px'
|
||||
}
|
||||
}).component();
|
||||
const statusIcon = this._view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: cardIconPath!.light,
|
||||
iconHeight: 24,
|
||||
iconWidth: 24,
|
||||
height: 32,
|
||||
CSSStyles: { 'margin': '0 8px' }
|
||||
}).component();
|
||||
|
||||
const textContainer = this._view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
const textContainer = this._view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
|
||||
const cardTitleText = this._view.modelBuilder.text().withProps({ value: cardTitle }).withProps({
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
'width': '240px'
|
||||
}
|
||||
}).component();
|
||||
const cardTitleText = this._view.modelBuilder.text()
|
||||
.withProps({ value: cardTitle })
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
'width': '240px',
|
||||
}
|
||||
}).component();
|
||||
textContainer.addItem(cardTitleText);
|
||||
|
||||
const cardCount = this._view.modelBuilder.text().withProps({
|
||||
@@ -368,32 +347,31 @@ export class DashboardWidget {
|
||||
let warningContainer;
|
||||
let warningText;
|
||||
if (hasSubtext) {
|
||||
const warningIcon = this._view.modelBuilder.image().withProps({
|
||||
iconPath: IconPathHelper.warning,
|
||||
iconWidth: 12,
|
||||
iconHeight: 12,
|
||||
width: 12,
|
||||
height: 18
|
||||
}).component();
|
||||
const warningIcon = this._view.modelBuilder.image()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.warning,
|
||||
iconWidth: 12,
|
||||
iconHeight: 12,
|
||||
width: 12,
|
||||
height: 18,
|
||||
}).component();
|
||||
|
||||
const warningDescription = '';
|
||||
warningText = this._view.modelBuilder.text().withProps({ value: warningDescription }).withProps({
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'padding-left': '8px',
|
||||
}
|
||||
}).component();
|
||||
warningText = this._view.modelBuilder.text().withProps({ value: warningDescription })
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'padding-left': '8px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
warningContainer = this._view.modelBuilder.flexContainer().withItems([
|
||||
warningIcon,
|
||||
warningText
|
||||
], {
|
||||
flex: '0 0 auto'
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'align-items': 'center'
|
||||
}
|
||||
}).component();
|
||||
warningContainer = this._view.modelBuilder.flexContainer()
|
||||
.withItems(
|
||||
[warningIcon, warningText],
|
||||
{ flex: '0 0 auto' })
|
||||
.withProps({
|
||||
CSSStyles: { 'align-items': 'center' }
|
||||
}).component();
|
||||
|
||||
textContainer.addItem(warningContainer);
|
||||
}
|
||||
@@ -452,255 +430,243 @@ export class DashboardWidget {
|
||||
const statusContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column',
|
||||
width: '400px',
|
||||
height: '360px',
|
||||
height: '385px',
|
||||
justifyContent: 'flex-start',
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'border': '1px solid rgba(0, 0, 0, 0.1)',
|
||||
'padding': '16px'
|
||||
'padding': '10px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const statusContainerTitle = view.modelBuilder.text().withProps({
|
||||
value: loc.DATABASE_MIGRATION_STATUS,
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS
|
||||
}
|
||||
}).component();
|
||||
const statusContainerTitle = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.DATABASE_MIGRATION_STATUS,
|
||||
width: '100%',
|
||||
CSSStyles: { ...styles.SECTION_HEADER_CSS }
|
||||
}).component();
|
||||
|
||||
this._viewAllMigrationsButton = view.modelBuilder.hyperlink().withProps({
|
||||
label: loc.VIEW_ALL,
|
||||
url: '',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS
|
||||
}
|
||||
}).component();
|
||||
this._refreshButton = view.modelBuilder.button()
|
||||
.withProps({
|
||||
label: loc.REFRESH,
|
||||
iconPath: IconPathHelper.refresh,
|
||||
iconHeight: 16,
|
||||
iconWidth: 16,
|
||||
width: 70,
|
||||
CSSStyles: { 'float': 'right' }
|
||||
}).component();
|
||||
|
||||
this._disposables.push(this._viewAllMigrationsButton.onDidClick(async (e) => {
|
||||
const migrationStatus = await this.getCurrentMigrations();
|
||||
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(
|
||||
[
|
||||
const statusHeadingContainer = view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
statusContainerTitle,
|
||||
buttonContainer
|
||||
]
|
||||
).withLayout({
|
||||
flexFlow: 'row',
|
||||
alignItems: 'center'
|
||||
}).component();
|
||||
this._refreshButton,
|
||||
]).withLayout({
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
flexFlow: 'row',
|
||||
}).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) {
|
||||
await addAccountImage.updateCssStyles({
|
||||
'display': 'block'
|
||||
});
|
||||
await addAccountText.updateCssStyles({
|
||||
'display': 'block'
|
||||
});
|
||||
await addAccountButton.updateCssStyles({
|
||||
'display': 'block'
|
||||
});
|
||||
await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': 'hidden' });
|
||||
await this._viewAllMigrationsButton.updateCssStyles({ 'visibility': 'hidden' });
|
||||
}
|
||||
buttonContainer.addItem(
|
||||
await this.createServiceSelector(this._view));
|
||||
|
||||
this._selectServiceText = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.SELECT_SERVICE_MESSAGE,
|
||||
CSSStyles: {
|
||||
'font-size': '12px',
|
||||
'margin': '10px',
|
||||
'font-weight': '350',
|
||||
'text-align': 'center',
|
||||
'display': 'none'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const header = view.modelBuilder.flexContainer()
|
||||
.withItems([statusHeadingContainer, buttonContainer])
|
||||
.withLayout({ flexFlow: 'column', })
|
||||
.component();
|
||||
|
||||
this._migrationStatusCardsContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
height: '272px',
|
||||
})
|
||||
.withProps({ CSSStyles: { 'overflow': 'hidden auto' } })
|
||||
.component();
|
||||
|
||||
await this._updateSummaryStatus();
|
||||
|
||||
// in progress
|
||||
this._inProgressMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.inProgressMigration,
|
||||
loc.MIGRATION_IN_PROGRESS
|
||||
);
|
||||
this._disposables.push(this._inProgressMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING);
|
||||
dialog.initialize();
|
||||
}));
|
||||
loc.MIGRATION_IN_PROGRESS);
|
||||
this._disposables.push(
|
||||
this._inProgressMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.ONGOING,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._inProgressMigrationButton.container
|
||||
);
|
||||
this._inProgressMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// in progress warning
|
||||
this._inProgressWarningMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.inProgressMigration,
|
||||
loc.MIGRATION_IN_PROGRESS,
|
||||
true
|
||||
);
|
||||
this._disposables.push(this._inProgressWarningMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING);
|
||||
dialog.initialize();
|
||||
}));
|
||||
true);
|
||||
this._disposables.push(
|
||||
this._inProgressWarningMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.ONGOING,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._inProgressWarningMigrationButton.container
|
||||
);
|
||||
this._inProgressWarningMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// successful
|
||||
this._successfulMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.completedMigration,
|
||||
loc.MIGRATION_COMPLETED
|
||||
);
|
||||
this._disposables.push(this._successfulMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.SUCCEEDED);
|
||||
dialog.initialize();
|
||||
}));
|
||||
loc.MIGRATION_COMPLETED);
|
||||
this._disposables.push(
|
||||
this._successfulMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.SUCCEEDED,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._successfulMigrationButton.container
|
||||
);
|
||||
|
||||
this._successfulMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// completing
|
||||
this._completingMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.completingCutover,
|
||||
loc.MIGRATION_CUTOVER_CARD
|
||||
);
|
||||
this._disposables.push(this._completingMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.COMPLETING);
|
||||
dialog.initialize();
|
||||
}));
|
||||
loc.MIGRATION_CUTOVER_CARD);
|
||||
this._disposables.push(
|
||||
this._completingMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.COMPLETING,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._completingMigrationButton.container
|
||||
);
|
||||
this._completingMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
// failed
|
||||
this._failedMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.error,
|
||||
loc.MIGRATION_FAILED
|
||||
);
|
||||
this._disposables.push(this._failedMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.FAILED);
|
||||
dialog.initialize();
|
||||
}));
|
||||
loc.MIGRATION_FAILED);
|
||||
this._disposables.push(
|
||||
this._failedMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.FAILED,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._failedMigrationButton.container
|
||||
);
|
||||
this._failedMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
this._notStartedMigrationCard = this.createStatusCard(
|
||||
IconPathHelper.notStartedMigration,
|
||||
loc.MIGRATION_NOT_STARTED
|
||||
);
|
||||
this._disposables.push(this._notStartedMigrationCard.container.onDidClick((e) => {
|
||||
void vscode.window.showInformationMessage('Feature coming soon');
|
||||
}));
|
||||
// all migrations
|
||||
this._allMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.view,
|
||||
loc.VIEW_ALL);
|
||||
this._disposables.push(
|
||||
this._allMigrationButton.container.onDidClick(async (e) => {
|
||||
const dialog = new MigrationStatusDialog(
|
||||
this._context,
|
||||
AdsMigrationStatus.ALL,
|
||||
this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._allMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
|
||||
this._migrationStatusCardLoadingContainer = view.modelBuilder.loadingComponent().withItem(this._migrationStatusCardsContainer).component();
|
||||
|
||||
statusContainer.addItem(
|
||||
header, {
|
||||
CSSStyles: {
|
||||
'margin-bottom': '16px'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
statusContainer.addItem(addAccountImage, {});
|
||||
statusContainer.addItem(addAccountText, {});
|
||||
statusContainer.addItem(addAccountButton, {});
|
||||
this._migrationStatusCardLoadingContainer = view.modelBuilder.loadingComponent()
|
||||
.withItem(this._migrationStatusCardsContainer)
|
||||
.component();
|
||||
statusContainer.addItem(header, { CSSStyles: { 'margin-bottom': '10px' } });
|
||||
statusContainer.addItem(this._selectServiceText, {});
|
||||
statusContainer.addItem(this._migrationStatusCardLoadingContainer, {});
|
||||
return statusContainer;
|
||||
}
|
||||
|
||||
private async _updateSummaryStatus(): Promise<void> {
|
||||
const serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
|
||||
const isContextValid = isServiceContextValid(serviceContext);
|
||||
await this._selectServiceText.updateCssStyles({ 'display': isContextValid ? 'none' : 'block' });
|
||||
await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': isContextValid ? 'visible' : 'hidden' });
|
||||
this._refreshButton.enabled = isContextValid;
|
||||
}
|
||||
|
||||
private async createServiceSelector(view: azdata.ModelView): Promise<azdata.Component> {
|
||||
const serviceContextLabel = await getSelectedServiceStatus();
|
||||
this._serviceContextButton = view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.sqlMigrationService,
|
||||
iconHeight: 22,
|
||||
iconWidth: 22,
|
||||
label: serviceContextLabel,
|
||||
title: serviceContextLabel,
|
||||
description: loc.MIGRATION_SERVICE_DESCRIPTION,
|
||||
buttonType: azdata.ButtonType.Informational,
|
||||
width: 375,
|
||||
CSSStyles: { ...BUTTON_CSS },
|
||||
})
|
||||
.component();
|
||||
|
||||
this._disposables.push(
|
||||
this._serviceContextButton.onDidClick(async () => {
|
||||
const dialog = new SelectMigrationServiceDialog(this.onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
|
||||
return this._serviceContextButton;
|
||||
}
|
||||
|
||||
private createVideoLinks(view: azdata.ModelView): azdata.Component {
|
||||
const linksContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column',
|
||||
width: '400px',
|
||||
height: '360px',
|
||||
justifyContent: 'flex-start',
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'border': '1px solid rgba(0, 0, 0, 0.1)',
|
||||
'padding': '16px',
|
||||
'overflow': 'scroll',
|
||||
}
|
||||
}).component();
|
||||
const linksContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: '440px',
|
||||
height: '385px',
|
||||
justifyContent: 'flex-start',
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'border': '1px solid rgba(0, 0, 0, 0.1)',
|
||||
'padding': '10px',
|
||||
'overflow': 'scroll',
|
||||
}
|
||||
}).component();
|
||||
const titleComponent = view.modelBuilder.text().withProps({
|
||||
value: loc.HELP_TITLE,
|
||||
CSSStyles: {
|
||||
@@ -809,11 +775,12 @@ export class DashboardWidget {
|
||||
...styles.BODY_CSS
|
||||
}
|
||||
}).component();
|
||||
this._disposables.push(video1Container.onDidClick(async () => {
|
||||
if (linkMetaData.link) {
|
||||
await vscode.env.openExternal(vscode.Uri.parse(linkMetaData.link));
|
||||
}
|
||||
}));
|
||||
this._disposables.push(
|
||||
video1Container.onDidClick(async () => {
|
||||
if (linkMetaData.link) {
|
||||
await vscode.env.openExternal(vscode.Uri.parse(linkMetaData.link));
|
||||
}
|
||||
}));
|
||||
videosContainer.addItem(video1Container, {
|
||||
CSSStyles: {
|
||||
'background-image': `url(${vscode.Uri.file(<string>linkMetaData.iconPath?.light)})`,
|
||||
|
||||
@@ -22,7 +22,10 @@ export class SavedAssessmentDialog {
|
||||
private context: vscode.ExtensionContext;
|
||||
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.context = context;
|
||||
}
|
||||
@@ -53,7 +56,7 @@ export class SavedAssessmentDialog {
|
||||
|
||||
dialog.registerCloseValidator(async () => {
|
||||
if (this.stateModel.resumeAssessment) {
|
||||
if (!this.stateModel.loadSavedInfo()) {
|
||||
if (await !this.stateModel.loadSavedInfo()) {
|
||||
void vscode.window.showInformationMessage(constants.OPEN_SAVED_INFO_ERROR);
|
||||
return false;
|
||||
}
|
||||
@@ -77,7 +80,11 @@ export class SavedAssessmentDialog {
|
||||
}
|
||||
|
||||
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);
|
||||
this._isOpen = false;
|
||||
}
|
||||
@@ -103,11 +110,11 @@ export class SavedAssessmentDialog {
|
||||
checked: true
|
||||
}).component();
|
||||
|
||||
radioStart.onDidChangeCheckedState((e) => {
|
||||
this._disposables.push(radioStart.onDidChangeCheckedState((e) => {
|
||||
if (e) {
|
||||
this.stateModel.resumeAssessment = false;
|
||||
}
|
||||
});
|
||||
}));
|
||||
const radioContinue = view.modelBuilder.radioButton().withProps({
|
||||
label: constants.RESUME_SESSION,
|
||||
name: buttonGroup,
|
||||
@@ -117,11 +124,11 @@ export class SavedAssessmentDialog {
|
||||
checked: false
|
||||
}).component();
|
||||
|
||||
radioContinue.onDidChangeCheckedState((e) => {
|
||||
this._disposables.push(radioContinue.onDidChangeCheckedState((e) => {
|
||||
if (e) {
|
||||
this.stateModel.resumeAssessment = true;
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
const flex = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
|
||||
@@ -95,7 +95,14 @@ export class CreateSqlMigrationServiceDialog {
|
||||
try {
|
||||
clearDialogMessage(this._dialogObject);
|
||||
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) {
|
||||
this.setDialogMessage(`${this._createdMigrationService.error.code} : ${this._createdMigrationService.error.message}`);
|
||||
this._statusLoadingComponent.loading = false;
|
||||
@@ -490,7 +497,12 @@ export class CreateSqlMigrationServiceDialog {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
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;
|
||||
} catch (e) {
|
||||
this._dialogObject.message = {
|
||||
@@ -502,7 +514,13 @@ export class CreateSqlMigrationServiceDialog {
|
||||
}
|
||||
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) => {
|
||||
return node.nodeName;
|
||||
});
|
||||
@@ -536,7 +554,12 @@ export class CreateSqlMigrationServiceDialog {
|
||||
const subscription = this._model._targetSubscription;
|
||||
const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name;
|
||||
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({
|
||||
title: constants.COPY_KEY1,
|
||||
|
||||
@@ -7,10 +7,11 @@ import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
|
||||
import * as constants from '../../constants/strings';
|
||||
import { SqlManagedInstance } from '../../api/azure';
|
||||
import { getMigrationTargetInstance, SqlManagedInstance } from '../../api/azure';
|
||||
import { IconPathHelper } from '../../constants/iconPathHelper';
|
||||
import { convertByteSizeToReadableUnit, get12HourTime } from '../../api/utils';
|
||||
import * as styles from '../../constants/styles';
|
||||
import { isBlobMigration } from '../../constants/helper';
|
||||
export class ConfirmCutoverDialog {
|
||||
private _dialogObject!: azdata.window.Dialog;
|
||||
private _view!: azdata.ModelView;
|
||||
@@ -21,20 +22,17 @@ export class ConfirmCutoverDialog {
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
|
||||
let tab = azdata.window.createTab('');
|
||||
const tab = azdata.window.createTab('');
|
||||
tab.registerContent(async (view: azdata.ModelView) => {
|
||||
this._view = view;
|
||||
|
||||
const completeCutoverText = view.modelBuilder.text().withProps({
|
||||
value: constants.COMPLETE_CUTOVER,
|
||||
CSSStyles: {
|
||||
...styles.PAGE_TITLE_CSS
|
||||
}
|
||||
CSSStyles: { ...styles.PAGE_TITLE_CSS }
|
||||
}).component();
|
||||
|
||||
const sourceDatabaseText = view.modelBuilder.text().withProps({
|
||||
value: this.migrationCutoverModel._migration.migrationContext.properties.sourceDatabaseName,
|
||||
value: this.migrationCutoverModel._migration.properties.sourceDatabaseName,
|
||||
CSSStyles: {
|
||||
...styles.SMALL_NOTE_CSS,
|
||||
'margin': '4px 0px 8px'
|
||||
@@ -42,12 +40,9 @@ export class ConfirmCutoverDialog {
|
||||
}).component();
|
||||
|
||||
const separator = this._view.modelBuilder.separator().withProps({ width: '800px' }).component();
|
||||
|
||||
const helpMainText = this._view.modelBuilder.text().withProps({
|
||||
value: constants.CUTOVER_HELP_MAIN,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS
|
||||
}
|
||||
CSSStyles: { ...styles.BODY_CSS }
|
||||
}).component();
|
||||
|
||||
const helpStepsText = this._view.modelBuilder.text().withProps({
|
||||
@@ -58,8 +53,9 @@ export class ConfirmCutoverDialog {
|
||||
}
|
||||
}).component();
|
||||
|
||||
|
||||
const fileContainer = this.migrationCutoverModel.isBlobMigration() ? this.createBlobFileContainer() : this.createNetworkShareFileContainer();
|
||||
const fileContainer = isBlobMigration(this.migrationCutoverModel.migrationStatus)
|
||||
? this.createBlobFileContainer()
|
||||
: this.createNetworkShareFileContainer();
|
||||
|
||||
const confirmCheckbox = this._view.modelBuilder.checkBox().withProps({
|
||||
CSSStyles: {
|
||||
@@ -76,16 +72,19 @@ export class ConfirmCutoverDialog {
|
||||
const cutoverWarning = this._view.modelBuilder.infoBox().withProps({
|
||||
text: constants.COMPLETING_CUTOVER_WARNING,
|
||||
style: 'warning',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS
|
||||
}
|
||||
CSSStyles: { ...styles.BODY_CSS }
|
||||
}).component();
|
||||
|
||||
|
||||
let infoDisplay = 'none';
|
||||
if (this.migrationCutoverModel._migration.targetManagedInstance.id.toLocaleLowerCase().includes('managedinstances')
|
||||
&& (<SqlManagedInstance>this.migrationCutoverModel._migration.targetManagedInstance)?.sku?.tier === 'BusinessCritical') {
|
||||
infoDisplay = 'inline';
|
||||
if (this.migrationCutoverModel._migration.id.toLocaleLowerCase().includes('managedinstances')) {
|
||||
const targetInstance = await getMigrationTargetInstance(
|
||||
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({
|
||||
@@ -111,23 +110,18 @@ export class ConfirmCutoverDialog {
|
||||
businessCriticalInfoBox
|
||||
]).component();
|
||||
|
||||
|
||||
this._dialogObject.okButton.enabled = false;
|
||||
this._dialogObject.okButton.label = constants.COMPLETE_CUTOVER;
|
||||
this._disposables.push(this._dialogObject.okButton.onClick(async (e) => {
|
||||
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(
|
||||
[
|
||||
{
|
||||
component: container
|
||||
}
|
||||
],
|
||||
{
|
||||
horizontal: false
|
||||
}
|
||||
[{ component: container }],
|
||||
{ horizontal: false }
|
||||
);
|
||||
const form = formBuilder.withLayout({ width: '100%' }).component();
|
||||
|
||||
@@ -144,18 +138,14 @@ export class ConfirmCutoverDialog {
|
||||
|
||||
private createBlobFileContainer(): azdata.FlexContainer {
|
||||
const container = this._view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'margin': '8px 0'
|
||||
}
|
||||
CSSStyles: { 'margin': '8px 0' }
|
||||
}).component();
|
||||
|
||||
const containerHeading = this._view.modelBuilder.text().withProps({
|
||||
value: constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0),
|
||||
width: 250,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS
|
||||
}
|
||||
CSSStyles: { ...styles.LABEL_CSS }
|
||||
}).component();
|
||||
container.addItem(containerHeading, { flex: '0' });
|
||||
|
||||
const refreshButton = this._view.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.refresh,
|
||||
@@ -165,13 +155,7 @@ export class ConfirmCutoverDialog {
|
||||
height: 20,
|
||||
label: constants.REFRESH,
|
||||
}).component();
|
||||
|
||||
|
||||
container.addItem(containerHeading, {
|
||||
flex: '0'
|
||||
});
|
||||
|
||||
refreshButton.onDidClick(async e => {
|
||||
this._disposables.push(refreshButton.onDidClick(async e => {
|
||||
refreshLoader.loading = true;
|
||||
try {
|
||||
await this.migrationCutoverModel.fetchStatus();
|
||||
@@ -184,11 +168,8 @@ export class ConfirmCutoverDialog {
|
||||
} finally {
|
||||
refreshLoader.loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
container.addItem(refreshButton, {
|
||||
flex: '0'
|
||||
});
|
||||
}));
|
||||
container.addItem(refreshButton, { flex: '0' });
|
||||
|
||||
const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
|
||||
loading: false,
|
||||
@@ -197,10 +178,8 @@ export class ConfirmCutoverDialog {
|
||||
'margin-left': '8px'
|
||||
}
|
||||
}).component();
|
||||
container.addItem(refreshLoader, { flex: '0' });
|
||||
|
||||
container.addItem(refreshLoader, {
|
||||
flex: '0'
|
||||
});
|
||||
return container;
|
||||
}
|
||||
|
||||
@@ -227,23 +206,18 @@ export class ConfirmCutoverDialog {
|
||||
}
|
||||
}).component();
|
||||
|
||||
containerHeading.onDidClick(async e => {
|
||||
this._disposables.push(containerHeading.onDidClick(async e => {
|
||||
if (expanded) {
|
||||
containerHeading.iconPath = IconPathHelper.expandButtonClosed;
|
||||
containerHeading.iconHeight = 12;
|
||||
await fileTable.updateCssStyles({
|
||||
'display': 'none'
|
||||
});
|
||||
|
||||
await fileTable.updateCssStyles({ 'display': 'none' });
|
||||
} else {
|
||||
containerHeading.iconPath = IconPathHelper.expandButtonOpen;
|
||||
containerHeading.iconHeight = 8;
|
||||
await fileTable.updateCssStyles({
|
||||
'display': 'inline'
|
||||
});
|
||||
await fileTable.updateCssStyles({ 'display': 'inline' });
|
||||
}
|
||||
expanded = !expanded;
|
||||
});
|
||||
}));
|
||||
|
||||
const refreshButton = this._view.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.refresh,
|
||||
@@ -252,16 +226,12 @@ export class ConfirmCutoverDialog {
|
||||
width: 70,
|
||||
height: 20,
|
||||
label: constants.REFRESH,
|
||||
CSSStyles: {
|
||||
'margin-top': '13px'
|
||||
}
|
||||
CSSStyles: { 'margin-top': '13px' }
|
||||
}).component();
|
||||
|
||||
headingRow.addItem(containerHeading, {
|
||||
flex: '0'
|
||||
});
|
||||
headingRow.addItem(containerHeading, { flex: '0' });
|
||||
|
||||
refreshButton.onDidClick(async e => {
|
||||
this._disposables.push(refreshButton.onDidClick(async e => {
|
||||
refreshLoader.loading = true;
|
||||
try {
|
||||
await this.migrationCutoverModel.fetchStatus();
|
||||
@@ -276,11 +246,8 @@ export class ConfirmCutoverDialog {
|
||||
} finally {
|
||||
refreshLoader.loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
headingRow.addItem(refreshButton, {
|
||||
flex: '0'
|
||||
});
|
||||
}));
|
||||
headingRow.addItem(refreshButton, { flex: '0' });
|
||||
|
||||
const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
|
||||
loading: false,
|
||||
@@ -290,20 +257,13 @@ export class ConfirmCutoverDialog {
|
||||
'height': '13px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
headingRow.addItem(refreshLoader, {
|
||||
flex: '0'
|
||||
});
|
||||
|
||||
headingRow.addItem(refreshLoader, { flex: '0' });
|
||||
container.addItem(headingRow);
|
||||
|
||||
const lastScanCompleted = this._view.modelBuilder.text().withProps({
|
||||
value: constants.LAST_SCAN_COMPLETED(get12HourTime(new Date())),
|
||||
CSSStyles: {
|
||||
...styles.NOTE_CSS
|
||||
}
|
||||
CSSStyles: { ...styles.NOTE_CSS }
|
||||
}).component();
|
||||
|
||||
container.addItem(lastScanCompleted);
|
||||
|
||||
const fileTable = this._view.modelBuilder.table().withProps({
|
||||
@@ -327,9 +287,7 @@ export class ConfirmCutoverDialog {
|
||||
data: [],
|
||||
width: 400,
|
||||
height: 150,
|
||||
CSSStyles: {
|
||||
'display': 'none'
|
||||
}
|
||||
CSSStyles: { 'display': 'none' }
|
||||
}).component();
|
||||
container.addItem(fileTable);
|
||||
this.refreshFileTable(fileTable);
|
||||
@@ -347,9 +305,7 @@ export class ConfirmCutoverDialog {
|
||||
];
|
||||
});
|
||||
} else {
|
||||
fileTable.data = [
|
||||
[constants.NO_PENDING_BACKUPS]
|
||||
];
|
||||
fileTable.data = [[constants.NO_PENDING_BACKUPS]];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,26 +6,24 @@
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { IconPathHelper } from '../../constants/iconPathHelper';
|
||||
import { BackupFileInfoStatus, MigrationContext, MigrationStatus } from '../../models/migrationLocalStorage';
|
||||
import { BackupFileInfoStatus, MigrationServiceContext, MigrationStatus } from '../../models/migrationLocalStorage';
|
||||
import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
|
||||
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 { ConfirmCutoverDialog } from './confirmCutoverDialog';
|
||||
import { logError, TelemetryViews } from '../../telemtery';
|
||||
import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog';
|
||||
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;
|
||||
|
||||
export class MigrationCutoverDialog {
|
||||
private _context: vscode.ExtensionContext;
|
||||
private _dialogObject!: azdata.window.Dialog;
|
||||
private _view!: azdata.ModelView;
|
||||
private _model: MigrationCutoverDialogModel;
|
||||
private _migration: MigrationContext;
|
||||
|
||||
private _databaseTitleName!: azdata.TextComponent;
|
||||
private _cutoverButton!: azdata.ButtonComponent;
|
||||
@@ -52,7 +50,6 @@ export class MigrationCutoverDialog {
|
||||
|
||||
private _fileCount!: azdata.TextComponent;
|
||||
private _fileTable!: azdata.DeclarativeTableComponent;
|
||||
private _autoRefreshHandle!: any;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
private _emptyTableFill!: azdata.FlexContainer;
|
||||
|
||||
@@ -60,10 +57,13 @@ export class MigrationCutoverDialog {
|
||||
|
||||
readonly _infoFieldWidth: string = '250px';
|
||||
|
||||
constructor(context: vscode.ExtensionContext, migration: MigrationContext) {
|
||||
this._context = context;
|
||||
this._migration = migration;
|
||||
this._model = new MigrationCutoverDialogModel(migration);
|
||||
constructor(
|
||||
private readonly _context: vscode.ExtensionContext,
|
||||
private readonly _serviceContext: MigrationServiceContext,
|
||||
private readonly _migration: DatabaseMigration,
|
||||
private readonly _onClosedCallback: () => Promise<void>) {
|
||||
|
||||
this._model = new MigrationCutoverDialogModel(_serviceContext, _migration);
|
||||
this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 'wide');
|
||||
}
|
||||
|
||||
@@ -224,15 +224,14 @@ export class MigrationCutoverDialog {
|
||||
);
|
||||
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 { } });
|
||||
}));
|
||||
this._disposables.push(
|
||||
this._view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
|
||||
return view.initializeModel(form).then(async (value) => {
|
||||
await this.refreshStatus();
|
||||
});
|
||||
await view.initializeModel(form);
|
||||
await this.refreshStatus();
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
|
||||
}
|
||||
@@ -242,9 +241,6 @@ export class MigrationCutoverDialog {
|
||||
this._dialogObject.cancelButton.hidden = true;
|
||||
this._dialogObject.okButton.label = loc.CLOSE;
|
||||
|
||||
this._disposables.push(this._dialogObject.okButton.onClick(e => {
|
||||
clearInterval(this._autoRefreshHandle);
|
||||
}));
|
||||
azdata.window.openDialog(this._dialogObject);
|
||||
}
|
||||
|
||||
@@ -262,7 +258,7 @@ export class MigrationCutoverDialog {
|
||||
...styles.PAGE_TITLE_CSS
|
||||
},
|
||||
width: 950,
|
||||
value: this._model._migration.migrationContext.properties.sourceDatabaseName
|
||||
value: this._model._migration.properties.sourceDatabaseName
|
||||
}).component();
|
||||
|
||||
const databaseSubTitle = this._view.modelBuilder.text().withProps({
|
||||
@@ -282,8 +278,6 @@ export class MigrationCutoverDialog {
|
||||
width: 950
|
||||
}).component();
|
||||
|
||||
this.setAutoRefresh(refreshFrequency);
|
||||
|
||||
const titleLogoContainer = this._view.modelBuilder.flexContainer().withProps({
|
||||
width: 1000
|
||||
}).component();
|
||||
@@ -314,7 +308,7 @@ export class MigrationCutoverDialog {
|
||||
enabled: false,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'display': this._isOnlineMigration() ? 'block' : 'none'
|
||||
'display': isOfflineMigation(this._model._migration) ? 'none' : 'block'
|
||||
}
|
||||
}).component();
|
||||
|
||||
@@ -322,16 +316,13 @@ export class MigrationCutoverDialog {
|
||||
await this.refreshStatus();
|
||||
const dialog = new ConfirmCutoverDialog(this._model);
|
||||
await dialog.initialize();
|
||||
await this.refreshStatus();
|
||||
|
||||
if (this._model.CutoverError) {
|
||||
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CUTOVER_ERROR, this._model.CutoverError);
|
||||
}
|
||||
}));
|
||||
|
||||
headerActions.addItem(this._cutoverButton, {
|
||||
flex: '0'
|
||||
});
|
||||
headerActions.addItem(this._cutoverButton, { flex: '0' });
|
||||
|
||||
this._cancelButton = this._view.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.cancel,
|
||||
@@ -377,7 +368,11 @@ export class MigrationCutoverDialog {
|
||||
this._disposables.push(this._retryButton.onDidClick(
|
||||
async (e) => {
|
||||
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();
|
||||
}
|
||||
));
|
||||
@@ -397,12 +392,14 @@ export class MigrationCutoverDialog {
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._disposables.push(this._refreshButton.onDidClick(
|
||||
async (e) => await this.refreshStatus()));
|
||||
this._disposables.push(
|
||||
this._refreshButton.onDidClick(async (e) => {
|
||||
this._refreshButton.enabled = false;
|
||||
await this.refreshStatus();
|
||||
this._refreshButton.enabled = true;
|
||||
}));
|
||||
|
||||
headerActions.addItem(this._refreshButton, {
|
||||
flex: '0',
|
||||
});
|
||||
headerActions.addItem(this._refreshButton, { flex: '0' });
|
||||
|
||||
this._copyDatabaseMigrationDetails = this._view.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.copy,
|
||||
@@ -425,9 +422,7 @@ export class MigrationCutoverDialog {
|
||||
|
||||
headerActions.addItem(this._copyDatabaseMigrationDetails, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'margin-left': '5px'
|
||||
}
|
||||
CSSStyles: { 'margin-left': '5px' }
|
||||
});
|
||||
|
||||
// create new support request button. Hiding button until sql migration support has been setup.
|
||||
@@ -443,11 +438,11 @@ export class MigrationCutoverDialog {
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._newSupportRequest.onDidClick(async (e) => {
|
||||
const serviceId = this._model._migration.controller.id;
|
||||
this._disposables.push(this._newSupportRequest.onDidClick(async (e) => {
|
||||
const serviceId = this._model._migration.properties.migrationService;
|
||||
const supportUrl = `https://portal.azure.com/#resource${serviceId}/supportrequest`;
|
||||
await vscode.env.openExternal(vscode.Uri.parse(supportUrl));
|
||||
});
|
||||
}));
|
||||
|
||||
headerActions.addItem(this._newSupportRequest, {
|
||||
flex: '0',
|
||||
@@ -519,12 +514,12 @@ export class MigrationCutoverDialog {
|
||||
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
|
||||
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
|
||||
|
||||
const isBlobMigration = this._model.isBlobMigration();
|
||||
const _isBlobMigration = isBlobMigration(this._model._migration);
|
||||
const flexStatus = this._view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
|
||||
this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', isBlobMigration);
|
||||
this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', _isBlobMigration);
|
||||
this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
|
||||
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
|
||||
addInfoFieldToContainer(this._fullBackupFileOnInfoField, flexStatus);
|
||||
@@ -533,10 +528,10 @@ export class MigrationCutoverDialog {
|
||||
const flexFile = this._view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).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._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', isBlobMigration);
|
||||
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', !isBlobMigration);
|
||||
this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', _isBlobMigration);
|
||||
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', !_isBlobMigration);
|
||||
addInfoFieldToContainer(this._lastLSNInfoField, flexFile);
|
||||
addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
|
||||
addInfoFieldToContainer(this._lastAppliedBackupTakenOnInfoField, flexFile);
|
||||
@@ -561,33 +556,8 @@ export class MigrationCutoverDialog {
|
||||
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 {
|
||||
if (this._model.migrationOpStatus) {
|
||||
return (JSON.stringify(
|
||||
{
|
||||
'async-operation-details': this._model.migrationOpStatus,
|
||||
'details': this._model.migrationStatus
|
||||
}
|
||||
, undefined, 2));
|
||||
} else {
|
||||
return (JSON.stringify(this._model.migrationStatus, undefined, 2));
|
||||
}
|
||||
return JSON.stringify(this._model.migrationStatus, undefined, 2);
|
||||
}
|
||||
|
||||
private async refreshStatus(): Promise<void> {
|
||||
@@ -598,18 +568,13 @@ export class MigrationCutoverDialog {
|
||||
try {
|
||||
clearDialogMessage(this._dialogObject);
|
||||
|
||||
if (this._isOnlineMigration()) {
|
||||
await this._cutoverButton.updateCssStyles({
|
||||
'display': 'block'
|
||||
});
|
||||
}
|
||||
await this._cutoverButton.updateCssStyles(
|
||||
{ 'display': isOfflineMigation(this._model._migration) ? 'none' : 'block' });
|
||||
|
||||
this.isRefreshing = true;
|
||||
this._refreshLoader.loading = true;
|
||||
await this._model.fetchStatus();
|
||||
const errors = [];
|
||||
errors.push(this._model.migrationOpStatus.error?.message);
|
||||
errors.push(this._model._migration.asyncOperationResult?.error?.message);
|
||||
errors.push(this._model.migrationStatus.properties.provisioningError);
|
||||
errors.push(this._model.migrationStatus.properties.migrationFailureError?.message);
|
||||
errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []);
|
||||
@@ -626,12 +591,12 @@ export class MigrationCutoverDialog {
|
||||
description: this.getMigrationDetails()
|
||||
};
|
||||
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
|
||||
const sqlServerName = this._model._migration.sourceConnectionProfile.serverName;
|
||||
const sourceDatabaseName = this._model._migration.migrationContext.properties.sourceDatabaseName;
|
||||
const sqlServerName = this._model._migration.properties.sourceServerName;
|
||||
const sourceDatabaseName = this._model._migration.properties.sourceDatabaseName;
|
||||
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
|
||||
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
|
||||
const targetDatabaseName = this._model._migration.migrationContext.name;
|
||||
const targetServerName = this._model._migration.targetManagedInstance.name;
|
||||
const targetDatabaseName = this._model._migration.name;
|
||||
const targetServerName = getResourceName(this._model._migration.id);
|
||||
let targetServerVersion;
|
||||
if (this._model.migrationStatus.id.includes('managedInstances')) {
|
||||
targetServerVersion = loc.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
|
||||
@@ -644,30 +609,30 @@ export class MigrationCutoverDialog {
|
||||
|
||||
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()) {
|
||||
tableData.push(
|
||||
...activeBackupSet.listOfBackupFiles.map(f => {
|
||||
return {
|
||||
fileName: f.fileName,
|
||||
type: activeBackupSet.backupType,
|
||||
status: f.status,
|
||||
dataUploaded: `${convertByteSizeToReadableUnit(f.dataWritten)} / ${convertByteSizeToReadableUnit(f.totalSize)}`,
|
||||
copyThroughput: (f.copyThroughput) ? (f.copyThroughput / 1024).toFixed(2) : '-',
|
||||
backupStartTime: activeBackupSet.backupStartDate,
|
||||
firstLSN: activeBackupSet.firstLSN,
|
||||
lastLSN: activeBackupSet.lastLSN
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (activeBackupSet.listOfBackupFiles[0].fileName === this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
|
||||
lastAppliedSSN = activeBackupSet.lastLSN;
|
||||
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
|
||||
}
|
||||
});
|
||||
if (activeBackupSet.listOfBackupFiles[0].fileName === this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
|
||||
lastAppliedSSN = activeBackupSet.lastLSN;
|
||||
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
|
||||
}
|
||||
});
|
||||
|
||||
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
|
||||
this._sourceDetailsInfoField.text.value = sqlServerName;
|
||||
@@ -677,21 +642,23 @@ export class MigrationCutoverDialog {
|
||||
this._targetServerInfoField.text.value = targetServerName;
|
||||
this._targetVersionInfoField.text.value = targetServerVersion;
|
||||
|
||||
const migrationStatusTextValue = this.getMigrationStatus();
|
||||
const migrationStatusTextValue = this._getMigrationStatus();
|
||||
this._migrationStatusInfoField.text.value = migrationStatusTextValue ?? '-';
|
||||
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migrationStatusTextValue);
|
||||
|
||||
this._fullBackupFileOnInfoField.text.value = this._model.migrationStatus?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? '-';
|
||||
|
||||
let backupLocation;
|
||||
const isBlobMigration = this._model.isBlobMigration();
|
||||
const _isBlobMigration = isBlobMigration(this._model._migration);
|
||||
// Displaying storage accounts and blob container for azure blob backups.
|
||||
if (isBlobMigration) {
|
||||
const storageAccountResourceId = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId;
|
||||
const blobContainerName = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName;
|
||||
backupLocation = `${storageAccountResourceId?.split('/').pop()} - ${blobContainerName}`;
|
||||
if (_isBlobMigration) {
|
||||
const storageAccountResourceId = this._model._migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId;
|
||||
const blobContainerName = this._model._migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName;
|
||||
backupLocation = storageAccountResourceId && blobContainerName
|
||||
? `${storageAccountResourceId?.split('/').pop()} - ${blobContainerName}`
|
||||
: undefined;
|
||||
} else {
|
||||
const fileShare = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.fileShare;
|
||||
const fileShare = this._model._migration.properties.backupConfiguration?.sourceLocation?.fileShare;
|
||||
backupLocation = fileShare?.path! ?? '-';
|
||||
}
|
||||
this._backupLocationInfoField.text.value = backupLocation ?? '-';
|
||||
@@ -700,7 +667,7 @@ export class MigrationCutoverDialog {
|
||||
this._lastAppliedBackupInfoField.text.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename ?? '-';
|
||||
this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : '-';
|
||||
|
||||
if (isBlobMigration) {
|
||||
if (_isBlobMigration) {
|
||||
if (!this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) {
|
||||
this._currentRestoringFileInfoField.text.value = '-';
|
||||
} else if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename === this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) {
|
||||
@@ -752,7 +719,7 @@ export class MigrationCutoverDialog {
|
||||
|
||||
this._cutoverButton.enabled = false;
|
||||
if (migrationStatusTextValue === MigrationStatus.InProgress) {
|
||||
if (isBlobMigration) {
|
||||
if (_isBlobMigration) {
|
||||
if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
|
||||
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 {
|
||||
return !this._model.isBlobMigration();
|
||||
return !isBlobMigration(this._model._migration);
|
||||
}
|
||||
|
||||
private getMigrationStatus(): string {
|
||||
if (this._model.migrationStatus) {
|
||||
return this._model.migrationStatus.properties.migrationStatus
|
||||
?? this._model.migrationStatus.properties.provisioningState;
|
||||
}
|
||||
return this._model._migration.migrationContext.properties.migrationStatus
|
||||
?? this._model._migration.migrationContext.properties.provisioningState;
|
||||
private _getMigrationStatus(): string {
|
||||
return this._model.migrationStatus
|
||||
? getMigrationStatus(this._model.migrationStatus)
|
||||
: getMigrationStatus(this._model._migration);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,43 +3,36 @@
|
||||
* 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 { BackupFileInfoStatus, MigrationContext } from '../../models/migrationLocalStorage';
|
||||
import { DatabaseMigration, startMigrationCutover, stopMigration, BackupFileInfo, getResourceGroupFromId, getMigrationDetails, getMigrationTargetName } from '../../api/azure';
|
||||
import { BackupFileInfoStatus, MigrationServiceContext } from '../../models/migrationLocalStorage';
|
||||
import { logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../../telemtery';
|
||||
import * as constants from '../../constants/strings';
|
||||
import { EOL } from 'os';
|
||||
import { getMigrationTargetType, getMigrationMode } from '../../constants/helper';
|
||||
import { getMigrationTargetType, getMigrationMode, isBlobMigration } from '../../constants/helper';
|
||||
|
||||
export class MigrationCutoverDialogModel {
|
||||
public CutoverError?: Error;
|
||||
public CancelMigrationError?: Error;
|
||||
|
||||
public migrationStatus!: DatabaseMigration;
|
||||
public migrationOpStatus!: AzureAsyncOperationResource;
|
||||
|
||||
constructor(public _migration: MigrationContext) {
|
||||
|
||||
constructor(
|
||||
public _serviceConstext: MigrationServiceContext,
|
||||
public _migration: DatabaseMigration
|
||||
) {
|
||||
}
|
||||
|
||||
public async fetchStatus(): Promise<void> {
|
||||
if (this._migration.asyncUrl) {
|
||||
this.migrationOpStatus = await getMigrationAsyncOperationDetails(
|
||||
this._migration.azureAccount,
|
||||
this._migration.subscription,
|
||||
this._migration.asyncUrl,
|
||||
this._migration.sessionId!);
|
||||
}
|
||||
|
||||
this.migrationStatus = await getMigrationStatus(
|
||||
this._migration.azureAccount,
|
||||
this._migration.subscription,
|
||||
this._migration.migrationContext,
|
||||
this._migration.sessionId!);
|
||||
this.migrationStatus = await getMigrationDetails(
|
||||
this._serviceConstext.azureAccount!,
|
||||
this._serviceConstext.subscription!,
|
||||
this._migration.id,
|
||||
this._migration.properties?.migrationOperationId);
|
||||
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.MigrationCutoverDialog,
|
||||
TelemetryAction.MigrationStatus,
|
||||
{
|
||||
'sessionId': this._migration.sessionId!,
|
||||
'migrationStatus': this.migrationStatus.properties?.migrationStatus
|
||||
},
|
||||
{}
|
||||
@@ -51,18 +44,16 @@ export class MigrationCutoverDialogModel {
|
||||
public async startCutover(): Promise<DatabaseMigration | undefined> {
|
||||
try {
|
||||
this.CutoverError = undefined;
|
||||
if (this.migrationStatus) {
|
||||
if (this._migration) {
|
||||
const cutover = await startMigrationCutover(
|
||||
this._migration.azureAccount,
|
||||
this._migration.subscription,
|
||||
this.migrationStatus,
|
||||
this._migration.sessionId!
|
||||
);
|
||||
this._serviceConstext.azureAccount!,
|
||||
this._serviceConstext.subscription!,
|
||||
this._migration!);
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.MigrationCutoverDialog,
|
||||
TelemetryAction.CutoverMigration,
|
||||
{
|
||||
...this.getTelemetryProps(this._migration),
|
||||
...this.getTelemetryProps(this._serviceConstext, this._migration),
|
||||
'migrationEndTime': new Date().toString(),
|
||||
},
|
||||
{}
|
||||
@@ -79,8 +70,6 @@ export class MigrationCutoverDialogModel {
|
||||
public async fetchErrors(): Promise<string> {
|
||||
const errors = [];
|
||||
await this.fetchStatus();
|
||||
errors.push(this.migrationOpStatus.error?.message);
|
||||
errors.push(this._migration.asyncOperationResult?.error?.message);
|
||||
errors.push(this.migrationStatus.properties.migrationFailureError?.message);
|
||||
return errors
|
||||
.filter((e, i, arr) => e !== undefined && i === arr.indexOf(e))
|
||||
@@ -93,18 +82,16 @@ export class MigrationCutoverDialogModel {
|
||||
if (this.migrationStatus) {
|
||||
const cutoverStartTime = new Date().toString();
|
||||
await stopMigration(
|
||||
this._migration.azureAccount,
|
||||
this._migration.subscription,
|
||||
this.migrationStatus,
|
||||
this._migration.sessionId!
|
||||
);
|
||||
this._serviceConstext.azureAccount!,
|
||||
this._serviceConstext.subscription!,
|
||||
this.migrationStatus);
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.MigrationCutoverDialog,
|
||||
TelemetryAction.CancelMigration,
|
||||
{
|
||||
...this.getTelemetryProps(this._migration),
|
||||
...this.getTelemetryProps(this._serviceConstext, this._migration),
|
||||
'migrationMode': getMigrationMode(this._migration),
|
||||
'cutoverStartTime': cutoverStartTime
|
||||
'cutoverStartTime': cutoverStartTime,
|
||||
},
|
||||
{}
|
||||
);
|
||||
@@ -116,12 +103,8 @@ export class MigrationCutoverDialogModel {
|
||||
return undefined!;
|
||||
}
|
||||
|
||||
public isBlobMigration(): boolean {
|
||||
return this._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.azureBlob !== undefined;
|
||||
}
|
||||
|
||||
public confirmCutoverStepsString(): string {
|
||||
if (this.isBlobMigration()) {
|
||||
if (isBlobMigration(this.migrationStatus)) {
|
||||
return `${constants.CUTOVER_HELP_STEP1}
|
||||
${constants.CUTOVER_HELP_STEP2_BLOB_CONTAINER}
|
||||
${constants.CUTOVER_HELP_STEP3_BLOB_CONTAINER}`;
|
||||
@@ -152,16 +135,15 @@ export class MigrationCutoverDialogModel {
|
||||
return files;
|
||||
}
|
||||
|
||||
private getTelemetryProps(migration: MigrationContext) {
|
||||
private getTelemetryProps(serviceContext: MigrationServiceContext, migration: DatabaseMigration) {
|
||||
return {
|
||||
'sessionId': migration.sessionId!,
|
||||
'subscriptionId': migration.subscription.id,
|
||||
'resourceGroup': getResourceGroupFromId(migration.targetManagedInstance.id),
|
||||
'sqlServerName': migration.sourceConnectionProfile.serverName,
|
||||
'sourceDatabaseName': migration.migrationContext.properties.sourceDatabaseName,
|
||||
'subscriptionId': serviceContext.subscription!.id,
|
||||
'resourceGroup': getResourceGroupFromId(migration.id),
|
||||
'sqlServerName': migration.properties.sourceServerName,
|
||||
'sourceDatabaseName': migration.properties.sourceDatabaseName,
|
||||
'targetType': getMigrationTargetType(migration),
|
||||
'targetDatabaseName': migration.migrationContext.name,
|
||||
'targetServerName': migration.targetManagedInstance.name,
|
||||
'targetDatabaseName': migration.name,
|
||||
'targetServerName': getMigrationTargetName(migration),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,22 +6,19 @@
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
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 { AdsMigrationStatus, MigrationStatusDialogModel } from './migrationStatusDialogModel';
|
||||
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 { ConfirmCutoverDialog } from '../migrationCutover/confirmCutoverDialog';
|
||||
import { MigrationCutoverDialogModel } from '../migrationCutover/migrationCutoverDialogModel';
|
||||
import { getMigrationTargetType, getMigrationMode, canRetryMigration } from '../../constants/helper';
|
||||
import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog';
|
||||
|
||||
const refreshFrequency: SupportedAutoRefreshIntervals = 180000;
|
||||
|
||||
const statusImageSize: number = 14;
|
||||
const imageCellStyles: azdata.CssStyles = { 'margin': '3px 3px 0 0', 'padding': '0' };
|
||||
const statusCellStyles: azdata.CssStyles = { 'margin': '0', 'padding': '0' };
|
||||
import { DatabaseMigration, getResourceName } from '../../api/azure';
|
||||
import { logError, TelemetryViews } from '../../telemtery';
|
||||
import { SelectMigrationServiceDialog } from '../selectMigrationService/selectMigrationServiceDialog';
|
||||
|
||||
const MenuCommands = {
|
||||
Cutover: 'sqlmigration.cutover',
|
||||
@@ -40,53 +37,56 @@ export class MigrationStatusDialog {
|
||||
private _view!: azdata.ModelView;
|
||||
private _searchBox!: azdata.InputBoxComponent;
|
||||
private _refresh!: azdata.ButtonComponent;
|
||||
private _serviceContextButton!: azdata.ButtonComponent;
|
||||
private _statusDropdown!: azdata.DropDownComponent;
|
||||
private _statusTable!: azdata.DeclarativeTableComponent;
|
||||
private _statusTable!: azdata.TableComponent;
|
||||
private _refreshLoader!: azdata.LoadingComponent;
|
||||
private _autoRefreshHandle!: NodeJS.Timeout;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
private _filteredMigrations: DatabaseMigration[] = [];
|
||||
|
||||
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._model = new MigrationStatusDialogModel(migrations);
|
||||
this._dialogObject = azdata.window.createModelViewDialog(loc.MIGRATION_STATUS, 'MigrationControllerDialog', 'wide');
|
||||
this._model = new MigrationStatusDialogModel([]);
|
||||
this._dialogObject = azdata.window.createModelViewDialog(
|
||||
loc.MIGRATION_STATUS,
|
||||
'MigrationControllerDialog',
|
||||
'wide');
|
||||
}
|
||||
|
||||
initialize() {
|
||||
async initialize() {
|
||||
let tab = azdata.window.createTab('');
|
||||
tab.registerContent(async (view: azdata.ModelView) => {
|
||||
this._view = view;
|
||||
this.registerCommands();
|
||||
const formBuilder = view.modelBuilder.formContainer().withFormItems(
|
||||
[
|
||||
{
|
||||
component: this.createSearchAndRefreshContainer()
|
||||
},
|
||||
{
|
||||
component: this.createStatusTable()
|
||||
}
|
||||
],
|
||||
{
|
||||
horizontal: false
|
||||
}
|
||||
);
|
||||
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 { } });
|
||||
}));
|
||||
const form = view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
[
|
||||
{ component: await this.createSearchAndRefreshContainer() },
|
||||
{ component: this.createStatusTable() }
|
||||
],
|
||||
{ horizontal: false }
|
||||
).withLayout({ width: '100%' })
|
||||
.component();
|
||||
this._disposables.push(
|
||||
this._view.onClosed(async e => {
|
||||
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.cancelButton.hidden = true;
|
||||
this._dialogObject.okButton.label = loc.CLOSE;
|
||||
this._disposables.push(this._dialogObject.okButton.onClick(e => {
|
||||
clearInterval(this._autoRefreshHandle);
|
||||
}));
|
||||
azdata.window.openDialog(this._dialogObject);
|
||||
}
|
||||
|
||||
@@ -100,111 +100,124 @@ export class MigrationStatusDialog {
|
||||
|
||||
private canCutoverMigration = (status: string | undefined) => status === MigrationStatus.InProgress;
|
||||
|
||||
private createSearchAndRefreshContainer(): azdata.FlexContainer {
|
||||
this._searchBox = this._view.modelBuilder.inputBox().withProps({
|
||||
stopEnterPropagation: true,
|
||||
placeHolder: loc.SEARCH_FOR_MIGRATIONS,
|
||||
width: '360px'
|
||||
}).component();
|
||||
|
||||
this._disposables.push(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();
|
||||
private async createSearchAndRefreshContainer(): Promise<azdata.FlexContainer> {
|
||||
this._searchBox = this._view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
stopEnterPropagation: true,
|
||||
placeHolder: loc.SEARCH_FOR_MIGRATIONS,
|
||||
width: '360px'
|
||||
}).component();
|
||||
this._disposables.push(
|
||||
this._searchBox.onTextChanged(
|
||||
async (value) => await this.populateMigrationTable()));
|
||||
|
||||
this._refresh = this._view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.refresh,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '20px',
|
||||
label: loc.REFRESH_BUTTON_LABEL,
|
||||
}).component();
|
||||
this._disposables.push(
|
||||
this._refresh.onDidClick(
|
||||
async (e) => { await this.refreshTable(); }));
|
||||
async (e) => await this.refreshTable()));
|
||||
|
||||
const flexContainer = this._view.modelBuilder.flexContainer().withProps({
|
||||
width: 900,
|
||||
CSSStyles: {
|
||||
'justify-content': 'left'
|
||||
},
|
||||
}).component();
|
||||
|
||||
flexContainer.addItem(this._searchBox, {
|
||||
flex: '0'
|
||||
});
|
||||
|
||||
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();
|
||||
}));
|
||||
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) {
|
||||
this._statusDropdown.value = (<azdata.CategoryValue[]>this._statusDropdown.values).find((value) => {
|
||||
return value.name === this._filter;
|
||||
});
|
||||
this._statusDropdown.value =
|
||||
(<azdata.CategoryValue[]>this._statusDropdown.values)
|
||||
.find(value => value.name === this._filter);
|
||||
}
|
||||
|
||||
flexContainer.addItem(this._statusDropdown, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'margin-left': '20px'
|
||||
}
|
||||
});
|
||||
this._refreshLoader = this._view.modelBuilder.loadingComponent()
|
||||
.withProps({ loading: false })
|
||||
.component();
|
||||
|
||||
flexContainer.addItem(this._refresh, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'margin-left': '20px'
|
||||
}
|
||||
});
|
||||
const searchLabel = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: 'Status',
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'font-weight': '600',
|
||||
'margin': '3px 0 0 0',
|
||||
},
|
||||
}).component();
|
||||
|
||||
this._refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
|
||||
loading: false,
|
||||
height: '55px'
|
||||
}).component();
|
||||
const serviceContextLabel = await getSelectedServiceStatus();
|
||||
this._serviceContextButton = this._view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconPath: IconPathHelper.sqlMigrationService,
|
||||
iconHeight: 22,
|
||||
iconWidth: 22,
|
||||
label: serviceContextLabel,
|
||||
title: serviceContextLabel,
|
||||
description: loc.MIGRATION_SERVICE_DESCRIPTION,
|
||||
buttonType: azdata.ButtonType.Informational,
|
||||
width: 270,
|
||||
}).component();
|
||||
|
||||
flexContainer.addItem(this._refreshLoader, {
|
||||
flex: '0 0 auto',
|
||||
CSSStyles: {
|
||||
'margin-left': '20px'
|
||||
}
|
||||
});
|
||||
this.setAutoRefresh(refreshFrequency);
|
||||
const container = this._view.modelBuilder.flexContainer().withProps({
|
||||
width: 1000
|
||||
}).component();
|
||||
container.addItem(flexContainer, {
|
||||
flex: '0 0 auto',
|
||||
CSSStyles: {
|
||||
'width': '980px'
|
||||
}
|
||||
});
|
||||
const onDialogClosed = async (): Promise<void> => {
|
||||
const label = await getSelectedServiceStatus();
|
||||
this._serviceContextButton.label = label;
|
||||
this._serviceContextButton.title = label;
|
||||
await this.refreshTable();
|
||||
};
|
||||
|
||||
this._disposables.push(
|
||||
this._serviceContextButton.onDidClick(
|
||||
async () => {
|
||||
const dialog = new SelectMigrationServiceDialog(onDialogClosed);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
this._disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.Cutover,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
clearDialogMessage(this._dialogObject);
|
||||
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
|
||||
if (this.canCutoverMigration(migration?.migrationContext.properties.migrationStatus)) {
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(migration!);
|
||||
const migration = this._model._migrations.find(
|
||||
migration => migration.id === migrationId);
|
||||
|
||||
if (this.canCutoverMigration(migration?.properties.migrationStatus)) {
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
const dialog = new ConfirmCutoverDialog(cutoverDialogModel);
|
||||
await dialog.initialize();
|
||||
@@ -224,8 +237,12 @@ export class MigrationStatusDialog {
|
||||
MenuCommands.ViewDatabase,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
|
||||
const dialog = new MigrationCutoverDialog(this._context, migration!);
|
||||
const migration = this._model._migrations.find(migration => migration.id === migrationId);
|
||||
const dialog = new MigrationCutoverDialog(
|
||||
this._context,
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!,
|
||||
this._onClosedCallback);
|
||||
await dialog.initialize();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
@@ -236,8 +253,8 @@ export class MigrationStatusDialog {
|
||||
MenuCommands.ViewTarget,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
|
||||
const url = 'https://portal.azure.com/#resource/' + migration!.targetManagedInstance.id;
|
||||
const migration = this._model._migrations.find(migration => migration.id === migrationId);
|
||||
const url = 'https://portal.azure.com/#resource/' + migration!.properties.scope;
|
||||
await vscode.env.openExternal(vscode.Uri.parse(url));
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
@@ -248,8 +265,10 @@ export class MigrationStatusDialog {
|
||||
MenuCommands.ViewService,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
|
||||
const dialog = new SqlMigrationServiceDetailsDialog(migration!);
|
||||
const migration = this._model._migrations.find(migration => migration.id === migrationId);
|
||||
const dialog = new SqlMigrationServiceDetailsDialog(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await dialog.initialize();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
@@ -261,17 +280,12 @@ export class MigrationStatusDialog {
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
clearDialogMessage(this._dialogObject);
|
||||
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(migration!);
|
||||
const migration = this._model._migrations.find(migration => migration.id === migrationId);
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
if (cutoverDialogModel.migrationOpStatus) {
|
||||
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.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migrationStatus, undefined, 2));
|
||||
|
||||
await vscode.window.showInformationMessage(loc.DETAILS_COPIED);
|
||||
} catch (e) {
|
||||
@@ -285,11 +299,13 @@ export class MigrationStatusDialog {
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
clearDialogMessage(this._dialogObject);
|
||||
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
|
||||
if (this.canCancelMigration(migration?.migrationContext.properties.migrationStatus)) {
|
||||
const migration = this._model._migrations.find(migration => migration.id === migrationId);
|
||||
if (this.canCancelMigration(migration?.properties.migrationStatus)) {
|
||||
void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => {
|
||||
if (v === loc.YES) {
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(migration!);
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
await cutoverDialogModel.cancelMigration();
|
||||
|
||||
@@ -312,9 +328,13 @@ export class MigrationStatusDialog {
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
clearDialogMessage(this._dialogObject);
|
||||
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
|
||||
if (canRetryMigration(migration?.migrationContext.properties.migrationStatus)) {
|
||||
let retryMigrationDialog = new RetryMigrationDialog(this._context, migration!);
|
||||
const migration = this._model._migrations.find(migration => migration.id === migrationId);
|
||||
if (canRetryMigration(migration?.properties.migrationStatus)) {
|
||||
let retryMigrationDialog = new RetryMigrationDialog(
|
||||
this._context,
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!,
|
||||
this._onClosedCallback);
|
||||
await retryMigrationDialog.openDialog();
|
||||
}
|
||||
else {
|
||||
@@ -329,75 +349,43 @@ export class MigrationStatusDialog {
|
||||
|
||||
private async populateMigrationTable(): Promise<void> {
|
||||
try {
|
||||
const migrations = filterMigrations(
|
||||
this._filteredMigrations = filterMigrations(
|
||||
this._model._migrations,
|
||||
(<azdata.CategoryValue>this._statusDropdown.value).name,
|
||||
this._searchBox.value!);
|
||||
|
||||
migrations.sort((m1, m2) => {
|
||||
return new Date(m1.migrationContext.properties?.startedOn) > new Date(m2.migrationContext.properties?.startedOn) ? -1 : 1;
|
||||
this._filteredMigrations.sort((m1, m2) => {
|
||||
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 [
|
||||
{ value: this._getDatabaserHyperLink(migration) },
|
||||
{ value: this._getMigrationStatus(migration) },
|
||||
{ value: getMigrationMode(migration) },
|
||||
{ value: getMigrationTargetType(migration) },
|
||||
{ value: migration.targetManagedInstance.name },
|
||||
{ value: migration.controller.name },
|
||||
{
|
||||
value: this._getMigrationDuration(
|
||||
migration.migrationContext.properties.startedOn,
|
||||
migration.migrationContext.properties.endedOn)
|
||||
},
|
||||
{ value: this._getMigrationTime(migration.migrationContext.properties.startedOn) },
|
||||
{ value: this._getMigrationTime(migration.migrationContext.properties.endedOn) },
|
||||
{
|
||||
value: {
|
||||
commands: this._getMenuCommands(migration),
|
||||
context: migration.migrationContext.id
|
||||
},
|
||||
}
|
||||
<azdata.HyperlinkColumnCellValue>{
|
||||
icon: IconPathHelper.sqlDatabaseLogo,
|
||||
title: migration.properties.sourceDatabaseName ?? '-',
|
||||
}, // database
|
||||
<azdata.HyperlinkColumnCellValue>{
|
||||
icon: getMigrationStatusImage(migration.properties.migrationStatus),
|
||||
title: this._getMigrationStatus(migration),
|
||||
}, // statue
|
||||
getMigrationMode(migration), // mode
|
||||
getMigrationTargetType(migration), // targetType
|
||||
getResourceName(migration.id), // targetName
|
||||
getResourceName(migration.properties.migrationService), // migrationService
|
||||
this._getMigrationDuration(
|
||||
migration.properties.startedOn,
|
||||
migration.properties.endedOn), // duration
|
||||
this._getMigrationTime(migration.properties.startedOn), // startTime
|
||||
this._getMigrationTime(migration.properties.endedOn), // endTime
|
||||
];
|
||||
});
|
||||
|
||||
await this._statusTable.setDataValues(data);
|
||||
await this._statusTable.updateProperty('data', data);
|
||||
} 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 {
|
||||
return migrationTime
|
||||
? new Date(migrationTime).toLocaleString()
|
||||
@@ -420,39 +408,11 @@ export class MigrationStatusDialog {
|
||||
return '---';
|
||||
}
|
||||
|
||||
private _getMenuCommands(migration: MigrationContext): string[] {
|
||||
const menuCommands: string[] = [];
|
||||
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;
|
||||
private _getMigrationStatus(migration: DatabaseMigration): string {
|
||||
const properties = migration.properties;
|
||||
const migrationStatus = properties.migrationStatus ?? properties.provisioningState;
|
||||
let warningCount = 0;
|
||||
if (migration.asyncOperationResult?.error?.message) {
|
||||
warningCount++;
|
||||
}
|
||||
|
||||
if (properties.migrationFailureError?.message) {
|
||||
warningCount++;
|
||||
}
|
||||
@@ -463,17 +423,16 @@ export class MigrationStatusDialog {
|
||||
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 {
|
||||
const dialog = azdata.window.createModelViewDialog(dialogHeading, dialogName, 288, 'callout', 'left', true, false,
|
||||
{
|
||||
xPos: 0,
|
||||
yPos: 0,
|
||||
width: 20,
|
||||
height: 20
|
||||
});
|
||||
const dialog = azdata.window.createModelViewDialog(dialogHeading, dialogName, 288, 'callout', 'left', true, false, {
|
||||
xPos: 0,
|
||||
yPos: 0,
|
||||
width: 20,
|
||||
height: 20
|
||||
});
|
||||
const tab: azdata.window.DialogTab = azdata.window.createTab('');
|
||||
tab.registerContent(async view => {
|
||||
const warningContentContainer = view.modelBuilder.divContainer().component();
|
||||
@@ -499,73 +458,6 @@ export class MigrationStatusDialog {
|
||||
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> {
|
||||
if (this.isRefreshing) {
|
||||
return;
|
||||
@@ -575,8 +467,7 @@ export class MigrationStatusDialog {
|
||||
try {
|
||||
clearDialogMessage(this._dialogObject);
|
||||
this._refreshLoader.loading = true;
|
||||
const currentConnection = await azdata.connection.getCurrentConnection();
|
||||
this._model._migrations = await MigrationLocalStorage.getMigrationsBySourceConnections(currentConnection, true);
|
||||
this._model._migrations = await getCurrentMigrations();
|
||||
await this.populateMigrationTable();
|
||||
} catch (e) {
|
||||
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_STATUS_REFRESH_ERROR, e);
|
||||
@@ -587,115 +478,111 @@ export class MigrationStatusDialog {
|
||||
}
|
||||
}
|
||||
|
||||
private createStatusTable(): azdata.DeclarativeTableComponent {
|
||||
const rowCssStyle: azdata.CssStyles = {
|
||||
'border': 'none',
|
||||
'text-align': 'left',
|
||||
'border-bottom': '1px solid',
|
||||
};
|
||||
private createStatusTable(): azdata.TableComponent {
|
||||
const headerCssStyles = undefined;
|
||||
const rowCssStyles = undefined;
|
||||
|
||||
const headerCssStyles: azdata.CssStyles = {
|
||||
'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({
|
||||
this._statusTable = this._view.modelBuilder.table().withProps({
|
||||
ariaLabel: loc.MIGRATION_STATUS,
|
||||
data: [],
|
||||
forceFitColumns: azdata.ColumnSizingMode.ForceFit,
|
||||
height: '600px',
|
||||
width: '1095px',
|
||||
display: 'grid',
|
||||
columns: [
|
||||
{
|
||||
displayName: loc.DATABASE,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
width: '90px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyles
|
||||
<azdata.HyperlinkColumn>{
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.DATABASE,
|
||||
value: 'database',
|
||||
width: 190,
|
||||
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,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
width: '170px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyles
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.MIGRATION_MODE,
|
||||
value: 'mode',
|
||||
width: 85,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
displayName: loc.MIGRATION_MODE,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '90px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyles
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.AZURE_SQL_TARGET,
|
||||
value: 'targetType',
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
displayName: loc.AZURE_SQL_TARGET,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '130px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyles
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.TARGET_AZURE_SQL_INSTANCE_NAME,
|
||||
value: 'targetName',
|
||||
width: 125,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
displayName: loc.TARGET_AZURE_SQL_INSTANCE_NAME,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '130px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyles
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.DATABASE_MIGRATION_SERVICE,
|
||||
value: 'migrationService',
|
||||
width: 140,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
displayName: loc.DATABASE_MIGRATION_SERVICE,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '150px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyles
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.DURATION,
|
||||
value: 'duration',
|
||||
width: 50,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
displayName: loc.DURATION,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '55px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyles
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.START_TIME,
|
||||
value: 'startTime',
|
||||
width: 115,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
displayName: loc.START_TIME,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '140px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyles
|
||||
cssClass: rowCssStyles,
|
||||
headerCssClass: headerCssStyles,
|
||||
name: loc.FINISH_TIME,
|
||||
value: 'finishTime',
|
||||
width: 115,
|
||||
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();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private _statusInfoMap(status: string): azdata.IconPath {
|
||||
return status === MigrationStatus.InProgress
|
||||
|| status === MigrationStatus.Creating
|
||||
|| status === MigrationStatus.Completing
|
||||
? IconPathHelper.warning
|
||||
: IconPathHelper.error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { DatabaseMigration } from '../../api/azure';
|
||||
import * as loc from '../../constants/strings';
|
||||
import { MigrationContext } from '../../models/migrationLocalStorage';
|
||||
|
||||
export class MigrationStatusDialogModel {
|
||||
public statusDropdownValues: azdata.CategoryValue[] = [
|
||||
@@ -27,7 +27,7 @@ export class MigrationStatusDialogModel {
|
||||
}
|
||||
];
|
||||
|
||||
constructor(public _migrations: MigrationContext[]) {
|
||||
constructor(public _migrations: DatabaseMigration[]) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,26 +7,26 @@ import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as mssql from 'mssql';
|
||||
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 { MigrationContext } from '../../models/migrationLocalStorage';
|
||||
import { MigrationServiceContext } from '../../models/migrationLocalStorage';
|
||||
import { WizardController } from '../../wizard/wizardController';
|
||||
import { getMigrationModeEnum, getMigrationTargetTypeEnum } from '../../constants/helper';
|
||||
import * as constants from '../../constants/strings';
|
||||
|
||||
export class RetryMigrationDialog {
|
||||
private _context: vscode.ExtensionContext;
|
||||
private _migration: MigrationContext;
|
||||
|
||||
constructor(context: vscode.ExtensionContext, migration: MigrationContext) {
|
||||
this._context = context;
|
||||
this._migration = migration;
|
||||
constructor(
|
||||
private readonly _context: vscode.ExtensionContext,
|
||||
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);
|
||||
|
||||
const sourceDatabaseName = migration.migrationContext.properties.sourceDatabaseName;
|
||||
const sourceDatabaseName = migration.properties.sourceDatabaseName;
|
||||
let savedInfo: SavedInfo;
|
||||
savedInfo = {
|
||||
closedPage: 0,
|
||||
@@ -41,53 +41,56 @@ export class RetryMigrationDialog {
|
||||
migrationTargetType: getMigrationTargetTypeEnum(migration)!,
|
||||
|
||||
// TargetSelection
|
||||
azureAccount: migration.azureAccount,
|
||||
azureTenant: migration.azureAccount.properties.tenants[0],
|
||||
subscription: migration.subscription,
|
||||
azureAccount: serviceContext.azureAccount!,
|
||||
azureTenant: serviceContext.azureAccount!.properties.tenants[0]!,
|
||||
subscription: serviceContext.subscription!,
|
||||
location: location,
|
||||
resourceGroup: {
|
||||
id: getFullResourceGroupFromId(migration.targetManagedInstance.id),
|
||||
name: getResourceGroupFromId(migration.targetManagedInstance.id),
|
||||
subscription: migration.subscription
|
||||
id: getFullResourceGroupFromId(migration.id),
|
||||
name: getResourceGroupFromId(migration.id),
|
||||
subscription: serviceContext.subscription!,
|
||||
},
|
||||
targetServerInstance: migration.targetManagedInstance,
|
||||
targetServerInstance: await getMigrationTargetInstance(
|
||||
serviceContext.azureAccount!,
|
||||
serviceContext.subscription!,
|
||||
migration),
|
||||
|
||||
// MigrationMode
|
||||
migrationMode: getMigrationModeEnum(migration),
|
||||
|
||||
// DatabaseBackup
|
||||
targetDatabaseNames: [migration.migrationContext.name],
|
||||
targetDatabaseNames: [migration.name],
|
||||
networkContainerType: null,
|
||||
networkShares: [],
|
||||
blobs: [],
|
||||
|
||||
// Integration Runtime
|
||||
sqlMigrationService: migration.controller,
|
||||
sqlMigrationService: serviceContext.migrationService,
|
||||
};
|
||||
|
||||
const getStorageAccountResourceGroup = (storageAccountResourceId: string) => {
|
||||
const getStorageAccountResourceGroup = (storageAccountResourceId: string): azureResource.AzureResourceResourceGroup => {
|
||||
return {
|
||||
id: getFullResourceGroupFromId(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);
|
||||
return {
|
||||
type: 'microsoft.storage/storageaccounts',
|
||||
id: storageAccountResourceId!,
|
||||
tenantId: savedInfo.azureTenant?.id!,
|
||||
subscriptionId: migration.subscription.id,
|
||||
subscriptionId: this._serviceContext.subscription?.id!,
|
||||
name: storageAccountName,
|
||||
location: savedInfo.location!.name,
|
||||
};
|
||||
};
|
||||
|
||||
const sourceLocation = migration.migrationContext.properties.backupConfiguration.sourceLocation;
|
||||
const sourceLocation = migration.properties.backupConfiguration?.sourceLocation;
|
||||
if (sourceLocation?.fileShare) {
|
||||
savedInfo.networkContainerType = NetworkContainerType.NETWORK_SHARE;
|
||||
const storageAccountResourceId = migration.migrationContext.properties.backupConfiguration.targetLocation?.storageAccountResourceId!;
|
||||
const storageAccountResourceId = migration.properties.backupConfiguration?.targetLocation?.storageAccountResourceId!;
|
||||
savedInfo.networkShares = [
|
||||
{
|
||||
password: '',
|
||||
@@ -106,9 +109,9 @@ export class RetryMigrationDialog {
|
||||
blobContainer: {
|
||||
id: getBlobContainerId(getFullResourceGroupFromId(storageAccountResourceId!), getResourceName(storageAccountResourceId!), 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!),
|
||||
resourceGroup: getStorageAccountResourceGroup(storageAccountResourceId!),
|
||||
storageKey: ''
|
||||
@@ -123,10 +126,18 @@ export class RetryMigrationDialog {
|
||||
}
|
||||
|
||||
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;
|
||||
locations.forEach(azureLocation => {
|
||||
if (azureLocation.name === this._migration.targetManagedInstance.location) {
|
||||
if (azureLocation.name === targetInstance.location) {
|
||||
location = azureLocation;
|
||||
}
|
||||
});
|
||||
@@ -146,10 +157,13 @@ export class RetryMigrationDialog {
|
||||
}
|
||||
|
||||
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()) {
|
||||
const wizardController = new WizardController(this._context, stateModel);
|
||||
if (await stateModel.loadSavedInfo()) {
|
||||
const wizardController = new WizardController(
|
||||
this._context,
|
||||
stateModel,
|
||||
this._onClosedCallback);
|
||||
await wizardController.openWizard(stateModel.sourceConnectionId);
|
||||
} else {
|
||||
void vscode.window.showInformationMessage(constants.MIGRATION_CANNOT_RETRY);
|
||||
|
||||
@@ -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}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
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 * as constants from '../../constants/strings';
|
||||
import { MigrationContext } from '../../models/migrationLocalStorage';
|
||||
import { MigrationServiceContext } from '../../models/migrationLocalStorage';
|
||||
import * as styles from '../../constants/styles';
|
||||
|
||||
const CONTROL_MARGIN = '10px';
|
||||
@@ -28,7 +28,10 @@ export class SqlMigrationServiceDetailsDialog {
|
||||
private _migrationServiceAuthKeyTable!: azdata.DeclarativeTableComponent;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(private migrationContext: MigrationContext) {
|
||||
constructor(
|
||||
private _serviceContext: MigrationServiceContext,
|
||||
private _migration: DatabaseMigration) {
|
||||
|
||||
this._dialog = azdata.window.createModelViewDialog(
|
||||
'',
|
||||
'SqlMigrationServiceDetailsDialog',
|
||||
@@ -46,7 +49,8 @@ export class SqlMigrationServiceDetailsDialog {
|
||||
|
||||
await this.createServiceContent(
|
||||
view,
|
||||
this.migrationContext);
|
||||
this._serviceContext,
|
||||
this._migration);
|
||||
});
|
||||
|
||||
this._dialog.okButton.label = constants.SQL_MIGRATION_SERVICE_DETAILS_BUTTON_LABEL;
|
||||
@@ -55,33 +59,31 @@ export class SqlMigrationServiceDetailsDialog {
|
||||
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);
|
||||
const serviceNode = (await getSqlMigrationServiceMonitoringData(
|
||||
migrationContext.azureAccount,
|
||||
migrationContext.subscription,
|
||||
migrationContext.controller.properties.resourceGroup,
|
||||
migrationContext.controller.location,
|
||||
migrationContext.controller.name,
|
||||
this.migrationContext.sessionId!
|
||||
));
|
||||
serviceContext.azureAccount!,
|
||||
serviceContext.subscription!,
|
||||
serviceContext.migrationService?.properties.resourceGroup!,
|
||||
serviceContext.migrationService?.location!,
|
||||
serviceContext.migrationService?.name!));
|
||||
const serviceNodeName = serviceNode.nodes?.map(node => node.nodeName).join(', ')
|
||||
|| constants.SQL_MIGRATION_SERVICE_DETAILS_STATUS_UNAVAILABLE;
|
||||
|
||||
const flexContainer = view.modelBuilder
|
||||
.flexContainer()
|
||||
.withItems([
|
||||
this._createHeading(view, migrationContext),
|
||||
this._createHeading(view, this._migration),
|
||||
view.modelBuilder
|
||||
.separator()
|
||||
.withProps({ width: STRETCH_WIDTH })
|
||||
.component(),
|
||||
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, 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, 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, serviceNodeName, VALUE_MARGIN),
|
||||
this._createTextItem(
|
||||
@@ -96,10 +98,10 @@ export class SqlMigrationServiceDetailsDialog {
|
||||
.component();
|
||||
|
||||
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
|
||||
.flexContainer()
|
||||
.withItems([
|
||||
@@ -120,19 +122,15 @@ export class SqlMigrationServiceDetailsDialog {
|
||||
view.modelBuilder
|
||||
.text()
|
||||
.withProps({
|
||||
value: migrationContext.controller.name,
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS
|
||||
}
|
||||
value: this._serviceContext.migrationService?.name,
|
||||
CSSStyles: { ...styles.SECTION_HEADER_CSS }
|
||||
})
|
||||
.component(),
|
||||
view.modelBuilder
|
||||
.text()
|
||||
.withProps({
|
||||
value: constants.SQL_MIGRATION_SERVICE_DETAILS_SUB_TITLE,
|
||||
CSSStyles: {
|
||||
...styles.SMALL_NOTE_CSS
|
||||
}
|
||||
CSSStyles: { ...styles.SMALL_NOTE_CSS }
|
||||
})
|
||||
.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(
|
||||
migrationContext.azureAccount,
|
||||
migrationContext.subscription,
|
||||
migrationContext.controller.properties.resourceGroup,
|
||||
migrationContext.controller.location.toUpperCase(),
|
||||
migrationContext.controller.name,
|
||||
keyName,
|
||||
migrationContext.sessionId!);
|
||||
serviceContext.azureAccount!,
|
||||
serviceContext.subscription!,
|
||||
serviceContext.migrationService?.properties.resourceGroup!,
|
||||
serviceContext.migrationService?.properties.location?.toUpperCase()!,
|
||||
serviceContext.migrationService?.name!,
|
||||
keyName);
|
||||
|
||||
if (keys?.authKey1 && keyName === AUTH_KEY1) {
|
||||
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));
|
||||
}
|
||||
|
||||
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(
|
||||
migrationContext.azureAccount,
|
||||
migrationContext.subscription,
|
||||
migrationContext.controller.properties.resourceGroup,
|
||||
migrationContext.controller.location.toUpperCase(),
|
||||
migrationContext.controller.name,
|
||||
migrationContext.sessionId!);
|
||||
serviceContext.azureAccount!,
|
||||
serviceContext.subscription!,
|
||||
serviceContext.migrationService?.properties.resourceGroup!,
|
||||
serviceContext.migrationService?.location.toUpperCase()!,
|
||||
serviceContext.migrationService?.name!);
|
||||
|
||||
const copyKey1Button = view.modelBuilder
|
||||
.button()
|
||||
@@ -275,7 +271,7 @@ export class SqlMigrationServiceDetailsDialog {
|
||||
})
|
||||
.component();
|
||||
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
|
||||
.button()
|
||||
@@ -288,7 +284,7 @@ export class SqlMigrationServiceDetailsDialog {
|
||||
})
|
||||
.component();
|
||||
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({
|
||||
dataValues: [
|
||||
|
||||
@@ -32,47 +32,54 @@ class SQLMigration {
|
||||
|
||||
async registerCommands(): Promise<void> {
|
||||
const commandDisposables: vscode.Disposable[] = [ // Array of disposables returned by registerCommand
|
||||
vscode.commands.registerCommand('sqlmigration.start', async () => {
|
||||
await this.launchMigrationWizard();
|
||||
}),
|
||||
vscode.commands.registerCommand('sqlmigration.openNotebooks', async () => {
|
||||
const input = vscode.window.createQuickPick<MigrationNotebookInfo>();
|
||||
input.placeholder = loc.NOTEBOOK_QUICK_PICK_PLACEHOLDER;
|
||||
vscode.commands.registerCommand(
|
||||
'sqlmigration.start',
|
||||
async () => await this.launchMigrationWizard()),
|
||||
vscode.commands.registerCommand(
|
||||
'sqlmigration.openNotebooks',
|
||||
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) => {
|
||||
const selectedNotebook = input.selectedItems[0];
|
||||
if (selectedNotebook) {
|
||||
try {
|
||||
await azdata.nb.showNotebookDocument(vscode.Uri.parse(`untitled: ${selectedNotebook.label}`), {
|
||||
preview: false,
|
||||
initialContent: (await fs.readFile(selectedNotebook.notebookPath)).toString(),
|
||||
initialDirtyState: false
|
||||
});
|
||||
} catch (e) {
|
||||
void vscode.window.showErrorMessage(`${loc.NOTEBOOK_OPEN_ERROR} - ${e.toString()}`);
|
||||
this.context.subscriptions.push(input.onDidAccept(async (e) => {
|
||||
const selectedNotebook = input.selectedItems[0];
|
||||
if (selectedNotebook) {
|
||||
try {
|
||||
await azdata.nb.showNotebookDocument(vscode.Uri.parse(`untitled: ${selectedNotebook.label}`), {
|
||||
preview: false,
|
||||
initialContent: (await fs.readFile(selectedNotebook.notebookPath)).toString(),
|
||||
initialDirtyState: false
|
||||
});
|
||||
} catch (e) {
|
||||
void vscode.window.showErrorMessage(`${loc.NOTEBOOK_OPEN_ERROR} - ${e.toString()}`);
|
||||
}
|
||||
input.hide();
|
||||
}
|
||||
input.hide();
|
||||
}
|
||||
}));
|
||||
}));
|
||||
|
||||
input.show();
|
||||
}),
|
||||
azdata.tasks.registerTask('sqlmigration.start', async () => {
|
||||
await this.launchMigrationWizard();
|
||||
}),
|
||||
azdata.tasks.registerTask('sqlmigration.newsupportrequest', async () => {
|
||||
await this.launchNewSupportRequest();
|
||||
}),
|
||||
azdata.tasks.registerTask('sqlmigration.sendfeedback', async () => {
|
||||
const actionId = 'workbench.action.openIssueReporter';
|
||||
const args = {
|
||||
extensionId: 'microsoft.sql-migration',
|
||||
issueTitle: loc.FEEDBACK_ISSUE_TITLE,
|
||||
};
|
||||
return await vscode.commands.executeCommand(actionId, args);
|
||||
}),
|
||||
input.show();
|
||||
}),
|
||||
azdata.tasks.registerTask(
|
||||
'sqlmigration.start',
|
||||
async () => await this.launchMigrationWizard()),
|
||||
azdata.tasks.registerTask(
|
||||
'sqlmigration.newsupportrequest',
|
||||
async () => await this.launchNewSupportRequest()),
|
||||
azdata.tasks.registerTask(
|
||||
'sqlmigration.sendfeedback',
|
||||
async () => {
|
||||
const actionId = 'workbench.action.openIssueReporter';
|
||||
const args = {
|
||||
extensionId: 'microsoft.sql-migration',
|
||||
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);
|
||||
@@ -97,14 +104,20 @@ class SQLMigration {
|
||||
if (api) {
|
||||
this.stateModel = new MigrationStateModel(this.context, connectionId, api.sqlMigration);
|
||||
this.context.subscriptions.push(this.stateModel);
|
||||
let savedInfo = this.checkSavedInfo(serverName);
|
||||
const savedInfo = this.checkSavedInfo(serverName);
|
||||
if (savedInfo) {
|
||||
this.stateModel.savedInfo = savedInfo;
|
||||
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();
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
@@ -131,10 +144,11 @@ class SQLMigration {
|
||||
}
|
||||
|
||||
let sqlMigration: SQLMigration;
|
||||
let widget: DashboardWidget;
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
sqlMigration = new SQLMigration(context);
|
||||
await sqlMigration.registerCommands();
|
||||
let widget = new DashboardWidget(context);
|
||||
widget = new DashboardWidget(context);
|
||||
widget.register();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as 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 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 {
|
||||
private static context: vscode.ExtensionContext;
|
||||
@@ -16,161 +18,75 @@ export class MigrationLocalStorage {
|
||||
MigrationLocalStorage.context = context;
|
||||
}
|
||||
|
||||
public static async getMigrationsBySourceConnections(connectionProfile: azdata.connection.ConnectionProfile, refreshStatus?: boolean): Promise<MigrationContext[]> {
|
||||
const undefinedSessionId = '{undefined}';
|
||||
const result: MigrationContext[] = [];
|
||||
const validMigrations: MigrationContext[] = [];
|
||||
const startTime = new Date().toString();
|
||||
// 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);
|
||||
public static async getMigrationServiceContext(): Promise<MigrationServiceContext> {
|
||||
const connectionProfile = await azdata.connection.getCurrentConnection();
|
||||
if (connectionProfile) {
|
||||
const serverContextKey = `${this.mementoToken}.${connectionProfile.serverName}.serviceContext`;
|
||||
return deepClone(await this.context.globalState.get(serverContextKey)) || {};
|
||||
}
|
||||
|
||||
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;
|
||||
return {};
|
||||
}
|
||||
|
||||
public static async refreshMigrationAzureAccount(migration: MigrationContext): Promise<void> {
|
||||
if (migration.azureAccount.isStale) {
|
||||
public static async saveMigrationServiceContext(serviceContext: MigrationServiceContext): Promise<void> {
|
||||
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 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) {
|
||||
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) {
|
||||
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 {
|
||||
sourceConnectionProfile: azdata.connection.ConnectionProfile,
|
||||
migrationContext: DatabaseMigration,
|
||||
targetManagedInstance: SqlManagedInstance | SqlVMServer,
|
||||
azureAccount: azdata.Account,
|
||||
subscription: azureResource.AzureResourceSubscription,
|
||||
controller: SqlMigrationService,
|
||||
asyncUrl: string,
|
||||
asyncOperationResult?: AzureAsyncOperationResource,
|
||||
sessionId?: string
|
||||
export function isServiceContextValid(serviceContext: MigrationServiceContext): boolean {
|
||||
return (
|
||||
serviceContext.azureAccount?.isStale === false &&
|
||||
serviceContext.location?.id !== undefined &&
|
||||
serviceContext.migrationService?.id !== undefined &&
|
||||
serviceContext.resourceGroup?.id !== undefined &&
|
||||
serviceContext.subscription?.id !== undefined &&
|
||||
serviceContext.tenant?.id !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -8,9 +8,8 @@ import { azureResource } from 'azureResource';
|
||||
import * as azurecore from 'azurecore';
|
||||
import * as vscode from 'vscode';
|
||||
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 { MigrationLocalStorage } from './migrationLocalStorage';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery';
|
||||
@@ -58,6 +57,12 @@ export enum NetworkContainerType {
|
||||
NETWORK_SHARE
|
||||
}
|
||||
|
||||
export enum FileStorageType {
|
||||
FileShare = 'FileShare',
|
||||
AzureBlob = 'AzureBlob',
|
||||
None = 'None',
|
||||
}
|
||||
|
||||
export enum Page {
|
||||
DatabaseSelector,
|
||||
SKURecommendation,
|
||||
@@ -826,8 +831,10 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
}
|
||||
accountValues = this._azureAccounts.map((account): azdata.CategoryValue => {
|
||||
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) {
|
||||
@@ -871,7 +878,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
public async getSubscriptionsDropdownValues(): Promise<azdata.CategoryValue[]> {
|
||||
let subscriptionsValues: azdata.CategoryValue[] = [];
|
||||
try {
|
||||
if (this._azureAccount) {
|
||||
if (this._azureAccount?.isStale === false) {
|
||||
this._subscriptions = await getSubscriptions(this._azureAccount);
|
||||
} else {
|
||||
this._subscriptions = [];
|
||||
@@ -1471,7 +1478,12 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
let sqlMigrationServiceValues: azdata.CategoryValue[] = [];
|
||||
try {
|
||||
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 {
|
||||
this._sqlMigrationServices = [];
|
||||
}
|
||||
@@ -1545,6 +1557,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
requestBody.properties.backupConfiguration = {
|
||||
targetLocation: undefined!,
|
||||
sourceLocation: {
|
||||
fileStorageType: 'AzureBlob',
|
||||
azureBlob: {
|
||||
storageAccountResourceId: this._databaseBackup.blobs[i].storageAccount.id,
|
||||
accountKey: this._databaseBackup.blobs[i].storageKey,
|
||||
@@ -1567,6 +1580,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
accountKey: this._databaseBackup.networkShares[i].storageKey,
|
||||
},
|
||||
sourceLocation: {
|
||||
fileStorageType: 'FileShare',
|
||||
fileShare: {
|
||||
path: this._databaseBackup.networkShares[i].networkShareLocation,
|
||||
username: this._databaseBackup.networkShares[i].windowsUser,
|
||||
@@ -1584,8 +1598,8 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
this._targetServerInstance,
|
||||
this._targetDatabaseNames[i],
|
||||
requestBody,
|
||||
this._sessionId
|
||||
);
|
||||
this._sessionId);
|
||||
|
||||
response.databaseMigration.properties.sourceDatabaseName = this._databasesForMigration[i];
|
||||
response.databaseMigration.properties.backupConfiguration = requestBody.properties.backupConfiguration!;
|
||||
response.databaseMigration.properties.offlineConfiguration = requestBody.properties.offlineConfiguration!;
|
||||
@@ -1621,22 +1635,18 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
}
|
||||
);
|
||||
|
||||
await MigrationLocalStorage.saveMigration(
|
||||
currentConnection!,
|
||||
response.databaseMigration,
|
||||
this._targetServerInstance,
|
||||
this._azureAccount,
|
||||
this._targetSubscription,
|
||||
this._sqlMigrationService!,
|
||||
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]));
|
||||
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) {
|
||||
void vscode.window.showErrorMessage(
|
||||
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 {
|
||||
// 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 {
|
||||
this._targetType = this.savedInfo.migrationTargetType || undefined!;
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ export enum TelemetryViews {
|
||||
SqlMigrationWizard = 'SqlMigrationWizard',
|
||||
MigrationLocalStorage = 'MigrationLocalStorage',
|
||||
SkuRecommendationWizard = 'SkuRecommendationWizard',
|
||||
DataCollectionWizard = 'GetAzureRecommendationDialog'
|
||||
DataCollectionWizard = 'GetAzureRecommendationDialog',
|
||||
SelectMigrationServiceDialog = 'SelectMigrationServiceDialog',
|
||||
}
|
||||
|
||||
export enum TelemetryAction {
|
||||
|
||||
@@ -786,15 +786,27 @@ export class DatabaseBackupPage extends MigrationWizardPage {
|
||||
await this.switchNetworkContainerFields(this.migrationStateModel._databaseBackup.networkContainerType);
|
||||
|
||||
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 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;
|
||||
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._sqlSourceUsernameInput.value = username;
|
||||
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._networkShareLocations = [];
|
||||
@@ -809,7 +821,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
|
||||
}
|
||||
|
||||
let originalTargetDatabaseNames = this.migrationStateModel._targetDatabaseNames;
|
||||
let originalNetworkShares = this.migrationStateModel._databaseBackup.networkShares;
|
||||
let originalNetworkShares = this.migrationStateModel._databaseBackup.networkShares || [];
|
||||
let originalBlobs = this.migrationStateModel._databaseBackup.blobs;
|
||||
if (this.migrationStateModel._didUpdateDatabasesForMigration) {
|
||||
this.migrationStateModel._targetDatabaseNames = [];
|
||||
@@ -830,7 +842,10 @@ export class DatabaseBackupPage extends MigrationWizardPage {
|
||||
blob = originalBlobs[dbIndex] ?? blob;
|
||||
} else {
|
||||
// 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) {
|
||||
networkShare = {
|
||||
...previouslySelectedNetworkShare,
|
||||
|
||||
@@ -8,43 +8,16 @@ import * as vscode from 'vscode';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
|
||||
import * as constants from '../constants/strings';
|
||||
import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { debounce } from '../api/utils';
|
||||
import * as styles from '../constants/styles';
|
||||
import { selectDatabasesFromList } from '../constants/helper';
|
||||
|
||||
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'
|
||||
};
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
|
||||
export class DatabaseSelectorPage extends MigrationWizardPage {
|
||||
private _view!: azdata.ModelView;
|
||||
private _databaseSelectorTable!: azdata.DeclarativeTableComponent;
|
||||
private _databaseSelectorTable!: azdata.TableComponent;
|
||||
private _dbNames!: string[];
|
||||
private _dbCount!: azdata.TextComponent;
|
||||
private _databaseTableValues!: azdata.DeclarativeTableCellValue[][];
|
||||
private _databaseTableValues!: any[];
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
|
||||
@@ -115,7 +88,6 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
}
|
||||
|
||||
|
||||
private createSearchComponent(): azdata.DivContainer {
|
||||
let resourceSearchBox = this._view.modelBuilder.inputBox().withProps({
|
||||
stopEnterPropagation: true,
|
||||
@@ -137,74 +109,36 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
|
||||
}
|
||||
|
||||
@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) {
|
||||
const filter: number[] = [];
|
||||
this._databaseTableValues.forEach((row, index) => {
|
||||
// undo when bug #16445 is fixed
|
||||
// const flexContainer: azdata.FlexContainer = row[1]?.value as azdata.FlexContainer;
|
||||
// const textComponent: azdata.TextComponent = flexContainer?.items[1] as azdata.TextComponent;
|
||||
// const cellText = textComponent?.value?.toLowerCase();
|
||||
const text = row[1]?.value as string;
|
||||
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);
|
||||
tableRows = this._databaseTableValues
|
||||
.filter(row => {
|
||||
const searchText = value?.toLowerCase();
|
||||
return row[2]?.toLowerCase()?.indexOf(searchText) > -1 // database name
|
||||
|| row[3]?.toLowerCase()?.indexOf(searchText) > -1 // state
|
||||
|| row[4]?.toLowerCase()?.indexOf(searchText) > -1 // size
|
||||
|| row[5]?.toLowerCase()?.indexOf(searchText) > -1; // last backup date
|
||||
});
|
||||
}
|
||||
|
||||
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> {
|
||||
const providerId = (await this.migrationStateModel.getSourceConnectionProfile()).providerId;
|
||||
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);
|
||||
}
|
||||
await this._loadDatabaseList(this.migrationStateModel, this.migrationStateModel._assessedDatabaseList);
|
||||
|
||||
const text = this._view.modelBuilder.text().withProps({
|
||||
value: constants.DATABASE_FOR_ASSESSMENT_DESCRIPTION,
|
||||
@@ -214,70 +148,83 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
|
||||
}).component();
|
||||
|
||||
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: {
|
||||
...styles.BODY_CSS,
|
||||
'margin-top': '8px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._databaseSelectorTable = this._view.modelBuilder.declarativeTable().withProps(
|
||||
{
|
||||
enableRowSelection: true,
|
||||
width: '100%',
|
||||
CSSStyles: {
|
||||
'border': 'none'
|
||||
},
|
||||
const cssClass = 'no-borders';
|
||||
this._databaseSelectorTable = this._view.modelBuilder.table()
|
||||
.withProps({
|
||||
data: [],
|
||||
width: 650,
|
||||
height: '100%',
|
||||
forceFitColumns: azdata.ColumnSizingMode.ForceFit,
|
||||
columns: [
|
||||
{
|
||||
displayName: '',
|
||||
valueType: azdata.DeclarativeDataType.boolean,
|
||||
width: 20,
|
||||
isReadOnly: false,
|
||||
showCheckAll: true,
|
||||
headerCssStyles: styleCheckBox
|
||||
<azdata.CheckboxColumn>{
|
||||
value: '',
|
||||
width: 10,
|
||||
type: azdata.ColumnType.checkBox,
|
||||
action: azdata.ActionOnCellCheckboxCheck.selectRow,
|
||||
resizable: false,
|
||||
cssClass: cssClass,
|
||||
headerCssClass: cssClass,
|
||||
},
|
||||
{
|
||||
displayName: constants.DATABASE,
|
||||
// undo when bug #16445 is fixed
|
||||
// valueType: azdata.DeclarativeDataType.component,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '100%',
|
||||
isReadOnly: true,
|
||||
headerCssStyles: styleLeft
|
||||
value: 'databaseicon',
|
||||
name: '',
|
||||
width: 10,
|
||||
type: azdata.ColumnType.icon,
|
||||
headerCssClass: cssClass,
|
||||
cssClass: cssClass,
|
||||
resizable: false,
|
||||
},
|
||||
{
|
||||
displayName: constants.STATUS,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: 100,
|
||||
isReadOnly: true,
|
||||
headerCssStyles: styleLeft
|
||||
name: constants.DATABASE,
|
||||
value: 'database',
|
||||
type: azdata.ColumnType.text,
|
||||
width: 360,
|
||||
cssClass: cssClass,
|
||||
headerCssClass: cssClass,
|
||||
},
|
||||
{
|
||||
displayName: constants.SIZE,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: 125,
|
||||
isReadOnly: true,
|
||||
headerCssStyles: styleRight
|
||||
name: constants.STATUS,
|
||||
value: 'status',
|
||||
type: azdata.ColumnType.text,
|
||||
width: 80,
|
||||
cssClass: cssClass,
|
||||
headerCssClass: cssClass,
|
||||
},
|
||||
{
|
||||
displayName: constants.LAST_BACKUP,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: 150,
|
||||
isReadOnly: true,
|
||||
headerCssStyles: styleLeft
|
||||
}
|
||||
name: constants.SIZE,
|
||||
value: 'size',
|
||||
type: azdata.ColumnType.text,
|
||||
width: 80,
|
||||
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);
|
||||
await this._databaseSelectorTable.setDataValues(this._databaseTableValues);
|
||||
await this.updateValuesOnSelection();
|
||||
|
||||
this._disposables.push(this._databaseSelectorTable.onDataChanged(async () => {
|
||||
this._disposables.push(this._databaseSelectorTable.onRowSelected(async (e) => {
|
||||
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({
|
||||
flexFlow: 'column',
|
||||
height: '100%',
|
||||
@@ -293,65 +240,60 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
|
||||
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[] {
|
||||
let result: string[] = [];
|
||||
this._databaseSelectorTable?.dataValues?.forEach((arr, index) => {
|
||||
if (arr[0].value === true) {
|
||||
result.push(this._dbNames[index]);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
const rows = this._databaseSelectorTable?.data || [];
|
||||
const databases = this._databaseSelectorTable?.selectedRows || [];
|
||||
return databases
|
||||
.filter(row => row < rows.length)
|
||||
.map(row => rows[row][2])
|
||||
|| [];
|
||||
}
|
||||
|
||||
private async updateValuesOnSelection() {
|
||||
const selectedDatabases = this.selectedDbs() || [];
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
@@ -44,27 +44,21 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
|
||||
protected async registerContent(view: azdata.ModelView): Promise<void> {
|
||||
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._statusLoadingComponent
|
||||
]).component();
|
||||
this._dmsInfoContainer = this._view.modelBuilder.flexContainer()
|
||||
.withItems([this._statusLoadingComponent])
|
||||
.component();
|
||||
|
||||
const form = view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
[
|
||||
{
|
||||
component: this.migrationServiceDropdownContainer()
|
||||
},
|
||||
{
|
||||
component: this._dmsInfoContainer
|
||||
}
|
||||
]
|
||||
).withProps({
|
||||
CSSStyles: {
|
||||
'padding-top': '0'
|
||||
}
|
||||
}).component();
|
||||
.withFormItems([
|
||||
{ component: this.migrationServiceDropdownContainer() },
|
||||
{ component: this._dmsInfoContainer }
|
||||
])
|
||||
.withProps({ CSSStyles: { 'padding-top': '0' } })
|
||||
.component();
|
||||
|
||||
this._disposables.push(this._view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
@@ -419,29 +413,26 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
|
||||
this.migrationStateModel._targetSubscription,
|
||||
this.migrationStateModel._sqlMigrationService.properties.resourceGroup,
|
||||
this.migrationStateModel._sqlMigrationService.location,
|
||||
this.migrationStateModel._sqlMigrationService.name,
|
||||
this.migrationStateModel._sessionId);
|
||||
this.migrationStateModel._sqlMigrationService.name);
|
||||
this.migrationStateModel._sqlMigrationService = migrationService;
|
||||
const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData(
|
||||
this.migrationStateModel._azureAccount,
|
||||
this.migrationStateModel._targetSubscription,
|
||||
this.migrationStateModel._sqlMigrationService.properties.resourceGroup,
|
||||
this.migrationStateModel._sqlMigrationService.location,
|
||||
this.migrationStateModel._sqlMigrationService!.name,
|
||||
this.migrationStateModel._sessionId);
|
||||
this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map(node => node.nodeName);
|
||||
this.migrationStateModel._sqlMigrationService!.name);
|
||||
this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map(
|
||||
node => node.nodeName);
|
||||
|
||||
const migrationServiceAuthKeys = await getSqlMigrationServiceAuthKeys(
|
||||
this.migrationStateModel._azureAccount,
|
||||
this.migrationStateModel._targetSubscription,
|
||||
this.migrationStateModel._sqlMigrationService.properties.resourceGroup,
|
||||
this.migrationStateModel._sqlMigrationService.location,
|
||||
this.migrationStateModel._sqlMigrationService!.name,
|
||||
this.migrationStateModel._sessionId
|
||||
);
|
||||
this.migrationStateModel._sqlMigrationService!.name);
|
||||
|
||||
this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map((node) => {
|
||||
return node.nodeName;
|
||||
});
|
||||
this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map(
|
||||
node => node.nodeName);
|
||||
|
||||
const state = migrationService.properties.integrationRuntimeState;
|
||||
if (state === 'Online') {
|
||||
@@ -458,25 +449,21 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
|
||||
|
||||
const data = [
|
||||
[
|
||||
{ value: constants.SERVICE_KEY1_LABEL },
|
||||
{ value: migrationServiceAuthKeys.authKey1 },
|
||||
{
|
||||
value: constants.SERVICE_KEY1_LABEL
|
||||
},
|
||||
{
|
||||
value: migrationServiceAuthKeys.authKey1
|
||||
},
|
||||
{
|
||||
value: this._view.modelBuilder.flexContainer().withItems([this._copy1, this._refresh1]).component()
|
||||
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: migrationServiceAuthKeys.authKey2
|
||||
},
|
||||
{
|
||||
value: this._view.modelBuilder.flexContainer().withItems([this._copy2, this._refresh2]).component()
|
||||
value: this._view.modelBuilder.flexContainer()
|
||||
.withItems([this._copy2, this._refresh2])
|
||||
.component()
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
@@ -186,7 +186,8 @@ export class TargetSelectionPage extends MigrationWizardPage {
|
||||
const selectedAzureAccount = this.migrationStateModel.getAccount(selectedIndex);
|
||||
// Making a clone of the account object to preserve the original tenants
|
||||
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._accountTenantDropdown.values = await this.migrationStateModel.getTenantValues();
|
||||
selectDropDownIndex(this._accountTenantDropdown, 0);
|
||||
|
||||
@@ -17,14 +17,17 @@ import { MigrationModePage } from './migrationModePage';
|
||||
import { DatabaseSelectorPage } from './databaseSelectorPage';
|
||||
import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery';
|
||||
import * as styles from '../constants/styles';
|
||||
import { MigrationLocalStorage, MigrationServiceContext } from '../models/migrationLocalStorage';
|
||||
import { azureResource } from 'azureResource';
|
||||
|
||||
export const WIZARD_INPUT_COMPONENT_WIDTH = '600px';
|
||||
export class WizardController {
|
||||
private _wizardObject!: azdata.window.Wizard;
|
||||
private _model!: MigrationStateModel;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
constructor(private readonly extensionContext: vscode.ExtensionContext, model: MigrationStateModel) {
|
||||
this._model = model;
|
||||
constructor(
|
||||
private readonly extensionContext: vscode.ExtensionContext,
|
||||
private readonly _model: MigrationStateModel,
|
||||
private readonly _onClosedCallback: () => Promise<void>) {
|
||||
}
|
||||
|
||||
public async openWizard(connectionId: string): Promise<void> {
|
||||
@@ -105,13 +108,12 @@ export class WizardController {
|
||||
});
|
||||
|
||||
await Promise.all(wizardSetupPromises);
|
||||
this._model.extensionContext.subscriptions.push(this._wizardObject.onPageChanged(async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => {
|
||||
await pages[0].onPageEnter(pageChangeInfo);
|
||||
}));
|
||||
this._model.extensionContext.subscriptions.push(
|
||||
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 () => {
|
||||
await stateModel.saveInfo(serverName, this._wizardObject.currentPage);
|
||||
await this._wizardObject.close();
|
||||
@@ -134,16 +136,65 @@ export class WizardController {
|
||||
|
||||
this._wizardObject.doneButton.label = loc.START_MIGRATION_TEXT;
|
||||
|
||||
this._disposables.push(this._wizardObject.doneButton.onClick(e => {
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.SqlMigrationWizard,
|
||||
TelemetryAction.PageButtonClick,
|
||||
{
|
||||
...this.getTelemetryProps(),
|
||||
'buttonPressed': TelemetryAction.Done,
|
||||
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
|
||||
}, {});
|
||||
}));
|
||||
this._disposables.push(
|
||||
this._wizardObject.doneButton.onClick(async (e) => {
|
||||
await stateModel.startMigration();
|
||||
await this.updateServiceContext(stateModel);
|
||||
await this._onClosedCallback();
|
||||
|
||||
sendSqlMigrationActionEvent(
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user