Adding migration status and cutover to extension (#14482)
3
extensions/sql-migration/images/cutover.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="13" height="14" viewBox="0 0 13 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1.7L10.2 7L1 12.3V1.7ZM0 0V14L12.3 7L0 0Z" fill="#0078D4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 B |
7
extensions/sql-migration/images/inProgress.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16Z" fill="#015CDA"/>
|
||||
<path d="M5 8.00002C5.00065 7.78641 5.02446 7.5735 5.071 7.36502L3.131 6.90002C3.04599 7.26058 3.00205 7.62959 3 8.00002C3.00517 8.41241 3.06161 8.82256 3.168 9.22102L5.109 8.75702C5.04013 8.51046 5.00349 8.25601 5 8.00002Z" fill="white"/>
|
||||
<path d="M5.68991 9.89099L4.12891 11.126C4.61194 11.7287 5.22805 12.2113 5.92891 12.536L6.76791 10.727C6.34933 10.5345 5.98052 10.2485 5.68991 9.89099Z" fill="white"/>
|
||||
<path d="M5.62545 6.18799C5.91388 5.80958 6.28818 5.50521 6.71745 5.29999L5.87845 3.48999C5.16699 3.82797 4.54544 4.32925 4.06445 4.95299L5.62545 6.18799Z" fill="white"/>
|
||||
<path d="M8 3V5C8.79565 5 9.55871 5.31607 10.1213 5.87868C10.6839 6.44129 11 7.20435 11 8C11 8.79565 10.6839 9.55871 10.1213 10.1213C9.55871 10.6839 8.79565 11 8 11V13C9.32608 13 10.5979 12.4732 11.5355 11.5355C12.4732 10.5979 13 9.32608 13 8C13 6.67392 12.4732 5.40215 11.5355 4.46447C10.5979 3.52678 9.32608 3 8 3Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
5
extensions/sql-migration/images/notStarted.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16Z" fill="#8A2DA5"/>
|
||||
<path d="M12.6321 3.36997C12.6321 3.36997 10.8941 3.28797 9.70109 4.48097C9.68909 4.49297 7.32409 6.85797 7.12009 7.06197C7.12009 7.06197 5.18009 6.57797 4.23109 7.52597L2.03809 9.72297L5.43909 8.76297L3.86309 10.925L5.07509 12.137L7.24409 10.572L6.27809 13.96L8.47509 11.767C9.43609 10.806 8.94409 8.87397 8.94409 8.87397C9.14609 8.67197 11.5071 6.30997 11.5191 6.29897C12.7121 5.10597 12.6321 3.36997 12.6321 3.36997Z" fill="white"/>
|
||||
<path d="M10.5 6.25C10.9142 6.25 11.25 5.91421 11.25 5.5C11.25 5.08579 10.9142 4.75 10.5 4.75C10.0858 4.75 9.75 5.08579 9.75 5.5C9.75 5.91421 10.0858 6.25 10.5 6.25Z" fill="#8A2DA5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 859 B |
23
extensions/sql-migration/images/sqlMI.svg
Normal file
@@ -0,0 +1,23 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.1733 14.8978C9.17102 15.0314 9.11691 15.159 9.02238 15.2535C8.92785 15.348 8.8003 15.4021 8.66664 15.4044H0.951083C0.816707 15.4044 0.687834 15.3511 0.592816 15.256C0.497798 15.161 0.444417 15.0321 0.444417 14.8978V1.04888C0.443212 0.982009 0.455494 0.915584 0.480529 0.853567C0.505565 0.791551 0.542841 0.735216 0.590132 0.687925C0.637422 0.640635 0.693758 0.603359 0.755774 0.578323C0.81779 0.553287 0.884215 0.541005 0.951083 0.54221H8.66664C8.80101 0.54221 8.92989 0.595591 9.02491 0.69061C9.11992 0.785628 9.1733 0.914501 9.1733 1.04888V14.8978Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M1.72461 5.75114C1.72693 5.50197 1.82694 5.26366 2.00314 5.08746C2.17934 4.91126 2.41766 4.81124 2.66683 4.80892H7.02239C7.14877 4.80537 7.27459 4.82706 7.3925 4.87271C7.5104 4.91836 7.61802 4.98705 7.70906 5.07479C7.8001 5.16252 7.87274 5.26752 7.92272 5.38365C7.97271 5.49978 7.99903 5.62472 8.00016 5.75114C8.00021 5.87832 7.97476 6.00422 7.9253 6.12139C7.87584 6.23855 7.80339 6.34461 7.71223 6.43329C7.62107 6.52197 7.51305 6.59146 7.39456 6.63766C7.27607 6.68386 7.14951 6.70582 7.02239 6.70226H2.66683C2.41612 6.69991 2.17648 6.59867 2.00003 6.42056C1.82358 6.24244 1.7246 6.00186 1.72461 5.75114Z" fill="#003067"/>
|
||||
<path d="M1.72461 2.9423C1.7246 2.69158 1.82358 2.451 2.00003 2.27289C2.17648 2.09477 2.41612 1.99353 2.66683 1.99119H7.02239C7.14951 1.98762 7.27607 2.00959 7.39456 2.05579C7.51305 2.10199 7.62107 2.17148 7.71223 2.26016C7.80339 2.34883 7.87584 2.45489 7.9253 2.57206C7.97476 2.68923 8.00021 2.81512 8.00016 2.9423C7.99903 3.06873 7.97271 3.19366 7.92272 3.30979C7.87274 3.42593 7.8001 3.53093 7.70906 3.61866C7.61802 3.70639 7.5104 3.77509 7.3925 3.82074C7.27459 3.86639 7.14877 3.88807 7.02239 3.88452H2.66683C2.41766 3.8822 2.17934 3.78219 2.00314 3.60599C1.82694 3.42979 1.72693 3.19147 1.72461 2.9423Z" fill="#003067"/>
|
||||
<path d="M2.72008 3.58215C3.07354 3.58215 3.36008 3.29562 3.36008 2.94215C3.36008 2.58869 3.07354 2.30215 2.72008 2.30215C2.36662 2.30215 2.08008 2.58869 2.08008 2.94215C2.08008 3.29562 2.36662 3.58215 2.72008 3.58215Z" fill="#50E6FF"/>
|
||||
<path d="M2.72008 6.39111C3.07354 6.39111 3.36008 6.10458 3.36008 5.75111C3.36008 5.39765 3.07354 5.11111 2.72008 5.11111C2.36662 5.11111 2.08008 5.39765 2.08008 5.75111C2.08008 6.10458 2.36662 6.39111 2.72008 6.39111Z" fill="#50E6FF"/>
|
||||
<path d="M15.5557 12.5156C15.5326 11.8097 15.26 11.1348 14.7865 10.6108C14.3129 10.0868 13.669 9.74759 12.969 9.65337C12.9183 8.67249 12.4849 7.75062 11.7619 7.0858C11.0389 6.42098 10.084 6.06625 9.10235 6.09782C8.30877 6.07883 7.5288 6.30625 6.86979 6.74877C6.21077 7.1913 5.7051 7.82719 5.42235 8.56893C4.57763 8.67064 3.79738 9.07188 3.22327 9.69981C2.64917 10.3277 2.31926 11.1407 2.29346 11.9912C2.3305 12.9453 2.7442 13.846 3.44396 14.4957C4.14371 15.1455 5.07247 15.4915 6.02679 15.4578H6.35568H12.4446H12.5957C13.3734 15.4443 14.1158 15.1305 14.6675 14.5821C15.2192 14.0337 15.5374 13.2933 15.5557 12.5156Z" fill="url(#paint1_linear)"/>
|
||||
<path d="M12.0976 12.8444V9.85777H11.271V13.52H13.4487V12.8444H12.0976ZM5.78652 11.3778C5.62374 11.3066 5.47127 11.214 5.33319 11.1022C5.29733 11.0643 5.26943 11.0196 5.25111 10.9708C5.23279 10.9219 5.22443 10.8699 5.22652 10.8178C5.22379 10.7656 5.23465 10.7136 5.25801 10.6669C5.28138 10.6201 5.31647 10.5802 5.35985 10.5511C5.46858 10.4742 5.60024 10.4366 5.73319 10.4444C6.04946 10.4328 6.36109 10.5232 6.62207 10.7022V9.93777C6.33592 9.84143 6.03502 9.7963 5.73319 9.80444C5.38601 9.78717 5.0436 9.89084 4.7643 10.0978C4.64654 10.1925 4.55235 10.3132 4.48912 10.4505C4.42588 10.5878 4.39533 10.7378 4.39985 10.8889C4.41424 11.1305 4.50191 11.362 4.65118 11.5526C4.80045 11.7432 5.00425 11.8837 5.23541 11.9555C5.43037 12.036 5.61292 12.1438 5.77763 12.2755C5.81919 12.3104 5.85264 12.3539 5.87567 12.4031C5.8987 12.4522 5.91074 12.5057 5.91096 12.56C5.91369 12.6122 5.90284 12.6642 5.87947 12.7109C5.8561 12.7576 5.82101 12.7975 5.77763 12.8267C5.65899 12.9038 5.5189 12.9411 5.37763 12.9333C5.0227 12.9307 4.6811 12.7978 4.41763 12.56V13.3778C4.70229 13.5175 5.01611 13.5876 5.33319 13.5822C5.69168 13.6096 6.04962 13.5224 6.35541 13.3333C6.48058 13.2402 6.58091 13.1177 6.64756 12.9766C6.71422 12.8356 6.74515 12.6803 6.73763 12.5244C6.74625 12.2962 6.66668 12.0734 6.51541 11.9022C6.31315 11.6768 6.06449 11.4979 5.78652 11.3778ZM10.3732 12.7822C10.5736 12.4436 10.6753 12.0556 10.6665 11.6622C10.6785 11.3168 10.602 10.9741 10.4443 10.6667C10.3082 10.3924 10.0951 10.1638 9.83097 10.0089C9.54548 9.84326 9.21846 9.76305 8.88874 9.77777C8.57614 9.79168 8.27139 9.88005 7.99985 10.0355C7.72201 10.1963 7.49664 10.434 7.35096 10.72C7.19247 11.0342 7.11025 11.3814 7.11096 11.7333C7.10582 12.0571 7.17901 12.3773 7.3243 12.6667C7.46198 12.937 7.67101 13.1644 7.92874 13.3244C8.19708 13.4873 8.50377 13.5763 8.81763 13.5822L9.57319 14.4711H10.6399L9.58208 13.4933C9.9184 13.3522 10.1972 13.1016 10.3732 12.7822ZM9.54652 12.56C9.46653 12.6616 9.36359 12.7427 9.24616 12.7968C9.12873 12.8509 9.00014 12.8763 8.87096 12.8711C8.74241 12.8746 8.61486 12.8474 8.49892 12.7917C8.38298 12.7361 8.28198 12.6536 8.2043 12.5511C8.01815 12.2939 7.92989 11.9787 7.95541 11.6622C7.92917 11.3443 8.02094 11.0279 8.21319 10.7733C8.29217 10.6672 8.39571 10.5819 8.51492 10.5246C8.63413 10.4673 8.76545 10.4398 8.89763 10.4444C9.02608 10.4395 9.1537 10.4671 9.26867 10.5246C9.38364 10.5821 9.48226 10.6676 9.55541 10.7733C9.73878 11.0319 9.82675 11.346 9.8043 11.6622C9.83192 11.983 9.74013 12.3027 9.54652 12.56Z" fill="#F2F2F2"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="4.80886" y1="15.4044" x2="4.80886" y2="0.542211" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#949494"/>
|
||||
<stop offset="0.53" stop-color="#A2A2A2"/>
|
||||
<stop offset="1" stop-color="#B3B3B3"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="8.92457" y1="15.4578" x2="8.92457" y2="6.06226" 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>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 187 KiB |
18
extensions/sql-migration/images/sqlMiVideoThumbnail.svg
Normal file
|
After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 166 KiB |
17
extensions/sql-migration/images/sqlVmVideoThumbnail.svg
Normal file
|
After Width: | Height: | Size: 107 KiB |
1
extensions/sql-migration/images/succeeded.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16"><defs><style>.cls-1{fill:none;clip-rule:evenodd;}.cls-2{clip-path:url(#clip-path);}.cls-3{fill:#3bb44a;}.cls-4{clip-path:url(#clip-path-2);}.cls-5{fill:#fff;}</style><clipPath id="clip-path"><path class="cls-1" d="M16,8a7.92,7.92,0,0,1-1.09,4A8.15,8.15,0,0,1,12,14.91a8,8,0,0,1-8.07,0A8.15,8.15,0,0,1,1.09,12,8,8,0,0,1,1.09,4,8.15,8.15,0,0,1,4,1.09a8,8,0,0,1,8.07,0A8.15,8.15,0,0,1,14.91,4,7.92,7.92,0,0,1,16,8Z"/></clipPath><clipPath id="clip-path-2"><polygon class="cls-1" points="10.9 4.9 11.6 5.6 6.5 10.71 3.65 7.85 4.35 7.15 6.5 9.29 10.9 4.9"/></clipPath></defs><title>success_complete</title><g class="cls-2"><rect class="cls-3" x="-5" y="-5" width="26" height="26"/></g><g class="cls-4"><rect class="cls-5" x="-1.35" y="-0.1" width="17.95" height="15.81"/></g></svg>
|
||||
|
After Width: | Height: | Size: 912 B |
@@ -14,7 +14,6 @@
|
||||
"activationEvents": [
|
||||
"onDashboardOpen",
|
||||
"onCommand:sqlmigration.start",
|
||||
"onCommand:sqlmigration.testDialog",
|
||||
"onCommand:sqlmigration.openNotebooks"
|
||||
],
|
||||
"main": "./out/main",
|
||||
@@ -32,11 +31,6 @@
|
||||
"title": "SQL Migration Start",
|
||||
"category": "SQL Migration"
|
||||
},
|
||||
{
|
||||
"command": "sqlmigration.testDialog",
|
||||
"title": "SQL Migration test dialog",
|
||||
"category": "SQL Migration"
|
||||
},
|
||||
{
|
||||
"command": "sqlmigration.openNotebooks",
|
||||
"title": "%migration-notebook-command-title%",
|
||||
@@ -60,8 +54,8 @@
|
||||
"name": "",
|
||||
"row": 0,
|
||||
"col": 1,
|
||||
"rowspan": 5,
|
||||
"colspan": 5,
|
||||
"rowspan": 2.5,
|
||||
"colspan": 3.5,
|
||||
"widget": {
|
||||
"modelview": {
|
||||
"id": "migration.dashboard"
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as azurecore from 'azurecore';
|
||||
import { azureResource } from 'azureResource';
|
||||
import * as loc from '../constants/strings';
|
||||
|
||||
async function getAzureCoreAPI(): Promise<azurecore.IExtension> {
|
||||
const api = (await vscode.extensions.getExtension(azurecore.extension.name)?.activate()) as azurecore.IExtension;
|
||||
@@ -82,7 +83,7 @@ export async function getBlobContainers(account: azdata.Account, subscription: S
|
||||
return blobContainers!;
|
||||
}
|
||||
|
||||
export async function getMigrationController(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise<MigrationController> {
|
||||
export async function getMigrationController(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise<SqlMigrationController> {
|
||||
const api = await getAzureCoreAPI();
|
||||
const host = `https://${regionName}.management.azure.com`;
|
||||
const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/Controllers/${controllerName}?api-version=2020-09-01-preview`;
|
||||
@@ -93,7 +94,7 @@ export async function getMigrationController(account: azdata.Account, subscripti
|
||||
return response.response.data;
|
||||
}
|
||||
|
||||
export async function getMigrationControllers(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string): Promise<MigrationController[]> {
|
||||
export async function getMigrationControllers(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string): Promise<SqlMigrationController[]> {
|
||||
const api = await getAzureCoreAPI();
|
||||
const host = `https://${regionName}.management.azure.com`;
|
||||
const path = `/subscriptions/${subscription.id}/providers/Microsoft.DataMigration/Controllers?api-version=2020-09-01-preview`;
|
||||
@@ -101,10 +102,11 @@ export async function getMigrationControllers(account: azdata.Account, subscript
|
||||
if (response.errors.length > 0) {
|
||||
throw new Error(response.errors.toString());
|
||||
}
|
||||
sortResourceArrayByName(response.response.data.value);
|
||||
return response.response.data.value;
|
||||
}
|
||||
|
||||
export async function createMigrationController(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise<MigrationController> {
|
||||
export async function createMigrationController(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise<SqlMigrationController> {
|
||||
const api = await getAzureCoreAPI();
|
||||
const host = `https://${regionName}.management.azure.com`;
|
||||
const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/Controllers/${controllerName}?api-version=2020-09-01-preview`;
|
||||
@@ -157,10 +159,10 @@ export async function getMigrationControllerMonitoringData(account: azdata.Accou
|
||||
return response.response.data;
|
||||
}
|
||||
|
||||
export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, managedInstance: string, migrationControllerName: string, requestBody: StartDatabaseMigrationRequest): Promise<StartDatabaseMigrationResponse> {
|
||||
export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, managedInstance: string, targetDatabaseName: string, requestBody: StartDatabaseMigrationRequest): Promise<StartDatabaseMigrationResponse> {
|
||||
const api = await getAzureCoreAPI();
|
||||
const host = `https://${regionName}.management.azure.com`;
|
||||
const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.Sql/managedInstances/${managedInstance}/providers/Microsoft.DataMigration/databaseMigrations/${migrationControllerName}?api-version=2020-09-01-preview`;
|
||||
const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.Sql/managedInstances/${managedInstance}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=2020-09-01-preview`;
|
||||
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, host);
|
||||
if (response.errors.length > 0) {
|
||||
throw new Error(response.errors.toString());
|
||||
@@ -171,7 +173,18 @@ export async function startDatabaseMigration(account: azdata.Account, subscripti
|
||||
};
|
||||
}
|
||||
|
||||
export async function getMigrationStatus(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise<any> {
|
||||
export async function getDatabaseMigration(account: azdata.Account, subscription: Subscription, regionName: string, migrationId: string): Promise<DatabaseMigration> {
|
||||
const api = await getAzureCoreAPI();
|
||||
const host = `https://${regionName}.management.azure.com`;
|
||||
const path = `${migrationId}?api-version=2020-09-01-preview`;
|
||||
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
|
||||
if (response.errors.length > 0) {
|
||||
throw new Error(response.errors.toString());
|
||||
}
|
||||
return response.response.data;
|
||||
}
|
||||
|
||||
export async function getMigrationStatus(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise<DatabaseMigration> {
|
||||
const api = await getAzureCoreAPI();
|
||||
const host = `https://eastus2euap.management.azure.com`;
|
||||
const path = `${migration.id}?$expand=MigrationStatusDetails&api-version=2020-09-01-preview`;
|
||||
@@ -179,9 +192,29 @@ export async function getMigrationStatus(account: azdata.Account, subscription:
|
||||
if (response.errors.length > 0) {
|
||||
throw new Error(response.errors.toString());
|
||||
}
|
||||
return {
|
||||
result: response.response.data
|
||||
};
|
||||
return response.response.data;
|
||||
}
|
||||
|
||||
export async function listMigrationsByController(account: azdata.Account, subscription: Subscription, controller: SqlMigrationController): Promise<DatabaseMigration[]> {
|
||||
const api = await getAzureCoreAPI();
|
||||
const host = `https://eastus2euap.management.azure.com`;
|
||||
const path = `${controller.id}/listMigrations?$expand=MigrationStatusDetails&api-version=2020-09-01-preview`;
|
||||
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
|
||||
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): Promise<any> {
|
||||
const api = await getAzureCoreAPI();
|
||||
const host = `https://eastus2euap.management.azure.com`;
|
||||
const path = `${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, host);
|
||||
if (response.errors.length > 0) {
|
||||
throw new Error(response.errors.toString());
|
||||
}
|
||||
return response.response.data.value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,13 +223,13 @@ export async function getMigrationStatus(account: azdata.Account, subscription:
|
||||
export function getMigrationControllerRegions(): azdata.CategoryValue[] {
|
||||
return [
|
||||
{
|
||||
displayName: 'East US EUAP',
|
||||
displayName: loc.EASTUS2EUAP,
|
||||
name: 'eastus2euap'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
type SortableAzureResources = AzureProduct | azureResource.FileShare | azureResource.BlobContainer | azureResource.AzureResourceSubscription;
|
||||
type SortableAzureResources = AzureProduct | azureResource.FileShare | azureResource.BlobContainer | azureResource.AzureResourceSubscription | SqlMigrationController;
|
||||
function sortResourceArrayByName(resourceArray: SortableAzureResources[]): void {
|
||||
if (!resourceArray) {
|
||||
return;
|
||||
@@ -222,7 +255,7 @@ export interface MigrationControllerProperties {
|
||||
isProvisioned?: boolean;
|
||||
}
|
||||
|
||||
export interface MigrationController {
|
||||
export interface SqlMigrationController {
|
||||
properties: MigrationControllerProperties;
|
||||
location: string;
|
||||
id: string;
|
||||
@@ -285,19 +318,105 @@ export interface StartDatabaseMigrationRequest {
|
||||
}
|
||||
}
|
||||
|
||||
export interface DatabaseMigration {
|
||||
properties: {
|
||||
name: string,
|
||||
provisioningState: string,
|
||||
sourceDatabaseName: string,
|
||||
migrationOperationId: string,
|
||||
},
|
||||
id: string,
|
||||
name: string,
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface StartDatabaseMigrationResponse {
|
||||
status: number,
|
||||
databaseMigration: DatabaseMigration
|
||||
}
|
||||
|
||||
export interface DatabaseMigration {
|
||||
properties: DatabaseMigrationProperties;
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
export interface DatabaseMigrationProperties {
|
||||
scope: string;
|
||||
provisioningState: string;
|
||||
migrationStatus: string;
|
||||
migrationStatusDetails?: MigrationStatusDetails;
|
||||
sourceSqlConnection: SqlConnectionInfo;
|
||||
sourceDatabaseName: string;
|
||||
targetDatabaseCollation: string;
|
||||
migrationController: string;
|
||||
migrationOperationId: string;
|
||||
backupConfiguration: BackupConfiguration;
|
||||
autoCutoverConfiguration: AutoCutoverConfiguration;
|
||||
migrationFailureError: ErrorInfo;
|
||||
}
|
||||
export interface MigrationStatusDetails {
|
||||
migrationState: string;
|
||||
startedOn: string;
|
||||
endedOn: string;
|
||||
fullBackupSetInfo: BackupSetInfo;
|
||||
lastRestoredBackupSetInfo: BackupSetInfo;
|
||||
activeBackupSets: BackupSetInfo[];
|
||||
blobContainerName: string;
|
||||
isFullBackupRestored: boolean;
|
||||
restoreBlockingReason: string;
|
||||
fileUploadBlockingErrors: string[];
|
||||
currentRestoringFileName: string;
|
||||
lastRestoredFilename: string;
|
||||
}
|
||||
|
||||
export interface SqlConnectionInfo {
|
||||
dataSource: string;
|
||||
authentication: string;
|
||||
username: string;
|
||||
password: string;
|
||||
encryptConnection: string;
|
||||
trustServerCertificate: string;
|
||||
}
|
||||
|
||||
export interface BackupConfiguration {
|
||||
sourceLocation: SourceLocation;
|
||||
targetLocation: TargetLocation;
|
||||
}
|
||||
|
||||
export interface AutoCutoverConfiguration {
|
||||
lastBackupName: string;
|
||||
}
|
||||
|
||||
export interface ErrorInfo {
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface BackupSetInfo {
|
||||
backupSetId: string;
|
||||
firstLSN: string;
|
||||
lastLSN: string;
|
||||
backupType: string;
|
||||
listOfBackupFiles: BackupFileInfo[];
|
||||
backupStartDate: string;
|
||||
backupFinishDate: string;
|
||||
isBackupRestored: boolean;
|
||||
backupSize: number;
|
||||
compressedBackupSize: number;
|
||||
}
|
||||
|
||||
export interface SourceLocation {
|
||||
fileShare: DatabaseMigrationFileShare;
|
||||
azureBlob: DatabaseMigrationAzureBlob;
|
||||
}
|
||||
|
||||
export interface TargetLocation {
|
||||
storageAccountResourceId: string;
|
||||
accountKey: string;
|
||||
}
|
||||
|
||||
export interface BackupFileInfo {
|
||||
fileName: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface DatabaseMigrationFileShare {
|
||||
path: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface DatabaseMigrationAzureBlob {
|
||||
storageAccountResourceId: string;
|
||||
accountKey: string;
|
||||
blobContainerName: string;
|
||||
}
|
||||
|
||||
@@ -13,10 +13,14 @@ export interface IconPath {
|
||||
export class IconPathHelper {
|
||||
public static copy: IconPath;
|
||||
public static refresh: IconPath;
|
||||
public static sqlMiImportHelpThumbnail: IconPath;
|
||||
public static sqlVmImportHelpThumbnail: IconPath;
|
||||
public static migrationDashboardHeaderBackground: IconPath;
|
||||
public static cutover: IconPath;
|
||||
public static sqlMigrationLogo: IconPath;
|
||||
public static sqlMiVideoThumbnail: IconPath;
|
||||
public static sqlVmVideoThumbnail: IconPath;
|
||||
public static migrationDashboardHeaderBackground: IconPath;
|
||||
public static inProgressMigration: IconPath;
|
||||
public static completedMigration: IconPath;
|
||||
public static notStartedMigration: IconPath;
|
||||
|
||||
public static setExtensionContext(context: vscode.ExtensionContext) {
|
||||
IconPathHelper.copy = {
|
||||
@@ -27,13 +31,13 @@ export class IconPathHelper {
|
||||
light: context.asAbsolutePath('images/refresh.svg'),
|
||||
dark: context.asAbsolutePath('images/refresh.svg')
|
||||
};
|
||||
IconPathHelper.sqlMiImportHelpThumbnail = {
|
||||
light: context.asAbsolutePath('images/sqlMiImportHelpThumbnail.svg'),
|
||||
dark: context.asAbsolutePath('images/sqlMiImportHelpThumbnail.svg')
|
||||
IconPathHelper.sqlMiVideoThumbnail = {
|
||||
light: context.asAbsolutePath('images/sqlMiVideoThumbnail.svg'),
|
||||
dark: context.asAbsolutePath('images/sqlMiVideoThumbnail.svg')
|
||||
};
|
||||
IconPathHelper.sqlVmImportHelpThumbnail = {
|
||||
light: context.asAbsolutePath('images/sqlVmImportHelpThumbnail.svg'),
|
||||
dark: context.asAbsolutePath('images/sqlVmImportHelpThumbnail.svg')
|
||||
IconPathHelper.sqlVmVideoThumbnail = {
|
||||
light: context.asAbsolutePath('images/sqlVmVideoThumbnail.svg'),
|
||||
dark: context.asAbsolutePath('images/sqlVmVideoThumbnail.svg')
|
||||
};
|
||||
IconPathHelper.migrationDashboardHeaderBackground = {
|
||||
light: context.asAbsolutePath('images/background.svg'),
|
||||
@@ -43,5 +47,21 @@ export class IconPathHelper {
|
||||
light: context.asAbsolutePath('images/migration.svg'),
|
||||
dark: context.asAbsolutePath('images/migration.svg')
|
||||
};
|
||||
IconPathHelper.inProgressMigration = {
|
||||
light: context.asAbsolutePath('images/inProgress.svg'),
|
||||
dark: context.asAbsolutePath('images/inProgress.svg')
|
||||
};
|
||||
IconPathHelper.completedMigration = {
|
||||
light: context.asAbsolutePath('images/succeeded.svg'),
|
||||
dark: context.asAbsolutePath('images/succeeded.svg')
|
||||
};
|
||||
IconPathHelper.notStartedMigration = {
|
||||
light: context.asAbsolutePath('images/notStarted.svg'),
|
||||
dark: context.asAbsolutePath('images/notStarted.svg')
|
||||
};
|
||||
IconPathHelper.cutover = {
|
||||
light: context.asAbsolutePath('images/cutover.svg'),
|
||||
dark: context.asAbsolutePath('images/cutover.svg')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as loc from '../models/strings';
|
||||
import * as loc from './strings';
|
||||
|
||||
export class NotebookPathHelper {
|
||||
private static context: vscode.ExtensionContext;
|
||||
|
||||
@@ -192,4 +192,52 @@ export const PRE_REQ_TITLE = localize('sql.migration.pre.req.title', "Things you
|
||||
export const PRE_REQ_1 = localize('sql.migration.pre.req.1', "Azure account details");
|
||||
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', "Migration in progress");
|
||||
export const LOG_SHIPPING_IN_PROGRESS = localize('sql.migration.log.shipping.in.progress', "Log shipping in progress");
|
||||
export const MIGRATION_COMPLETED = localize('sql.migration.migration.completed', "Migration completed");
|
||||
export const SUCCESSFULLY_MIGRATED_TO_AZURE_SQL = localize('sql.migration.successfully.migrated.to.azure.sql', "Successfully migrated to Azure SQL");
|
||||
export const MIGRATION_NOT_STARTED = localize('sql.migration.migration.not.started', "Migration not started");
|
||||
export const CHOOSE_TO_MIGRATE_TO_AZURE_SQL = localize('sql.migration.choose.to.migrate.to.azure.sql', "Choose to migrate to Azure SQL");
|
||||
|
||||
|
||||
// Azure APIs
|
||||
export const EASTUS2EUAP = localize('sql.migration.eastus2euap', 'East US 2 EUAP');
|
||||
|
||||
|
||||
//Migration cutover dialog
|
||||
export const MIGRATION_CUTOVER = localize('sql.migration.cutover', "Migration cutover");
|
||||
export const SOURCE_SERVER = localize('sql.migration.source.server', "Source server");
|
||||
export const SOURCE_VERSION = localize('sql.migration.source.version', "Source version");
|
||||
export const TARGET_SERVER = localize('sql.migration.target.server', "Target server");
|
||||
export const TARGET_VERSION = localize('sql.migration.target.version', "Target version");
|
||||
export const MIGRATION_STATUS = localize('sql.migration.migration.status', "Migration status");
|
||||
export const FULL_BACKUP_FILES = localize('sql.migration.full.backup.files', "Full backup files(s)");
|
||||
export const LAST_APPLIED_LSN = localize('sql.migration.last.applied.lsn', "Last applied LSN");
|
||||
export const LAST_APPLIED_BACKUP_FILES = localize('sql.migration.last.applied.backup.files', "Last applied backup file(s)");
|
||||
export const LAST_APPLIED_BACKUP_FILES_TAKEN_ON = localize('sql.migration.last.applied.files.taken.on', "Last applied backup file(s) taken on");
|
||||
export const ACTIVE_BACKUP_FILES = localize('sql.migration.active.backup.files', "Active Backup file(s)");
|
||||
export const STATUS = localize('sql.migration.status', "Status");
|
||||
export const BACKUP_START_TIME = localize('sql.migration.backup.start.time', "Backup start time");
|
||||
export const FIRST_LSN = localize('sql.migration.first.lsn', "First LSN");
|
||||
export const LAST_LSN = localize('sql.migration.last.LSN', "Last LSN");
|
||||
export const CANNOT_START_CUTOVER_ERROR = localize('sql.migration.cannot.start.cutover.error', "Cannot start the cutover process until all the migrations are done. Click refresh to fetch the latest file status");
|
||||
export const AZURE_SQL_DATABASE_MANAGED_INSTANCE = localize('sql.migration.azure.sql.database.managed.instance', "Azure SQL Database Managed Instance");
|
||||
export const AZURE_SQL_DATABASE_VIRTUAL_MACHINE = localize('sql.migration.azure.sql.database.virtual.machine', "Azure SQL Database Virtual Machine");
|
||||
|
||||
export function ACTIVE_BACKUP_FILES_ITEMS(fileCount: number) {
|
||||
if (fileCount === 1) {
|
||||
return localize('sql.migration.active.backup.files.items', "Active Backup files (1 item)");
|
||||
} else {
|
||||
return localize('sql.migration.active.backup.files.multiple.items', "Active Backup files ({0} items)", fileCount);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Migration status dialog
|
||||
export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migration', "Search for migrations");
|
||||
export const ONLINE = localize('sql.migration.online', "Online");
|
||||
export const DATABASE = localize('sql.migration.database', "Database");
|
||||
export const TARGET_AZURE_SQL_INSTANCE_NAME = localize('sql.migration.target.azure.sql.instance.name', "Target Azure SQL Instance Name");
|
||||
export const CUTOVER_TYPE = localize('sql.migration.cutover.type', "Cutover type");
|
||||
export const START_TIME = localize('sql.migration.start.time', "Start Time");
|
||||
export const FINISH_TIME = localize('sql.migration.finish.time', "Finish Time");
|
||||
@@ -5,9 +5,12 @@
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { MigrationLocalStorage } from '../models/migrationLocalStorage';
|
||||
import * as loc from '../models/strings';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { MigrationContext, MigrationLocalStorage } from '../models/migrationLocalStorage';
|
||||
import * as loc from '../constants/strings';
|
||||
import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { getDatabaseMigration } from '../api/azure';
|
||||
import { MigrationStatusDialog } from '../dialog/migrationStatus/migrationStatusDialog';
|
||||
import { MigrationCategory } from '../dialog/migrationStatus/migrationStatusDialogModel';
|
||||
|
||||
interface IActionMetadata {
|
||||
title?: string,
|
||||
@@ -22,6 +25,7 @@ const maxWidth = 800;
|
||||
export class DashboardWidget {
|
||||
|
||||
private _migrationStatusCardsContainer!: azdata.FlexContainer;
|
||||
private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent;
|
||||
private _view!: azdata.ModelView;
|
||||
|
||||
constructor() {
|
||||
@@ -30,47 +34,36 @@ export class DashboardWidget {
|
||||
public register(): void {
|
||||
azdata.ui.registerModelViewProvider('migration.dashboard', async (view) => {
|
||||
this._view = view;
|
||||
|
||||
const container = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}).component();
|
||||
|
||||
const header = this.createHeader(view);
|
||||
|
||||
const tasksContainer = await this.createTasks(view);
|
||||
|
||||
container.addItem(header, {
|
||||
CSSStyles: {
|
||||
'background-image': `url(${vscode.Uri.file(<string>IconPathHelper.migrationDashboardHeaderBackground.light)})`,
|
||||
'width': '1100px',
|
||||
'height': '300px',
|
||||
'width': '870px',
|
||||
'height': '260px',
|
||||
'background-size': '100%',
|
||||
}
|
||||
});
|
||||
|
||||
const tasksContainer = await this.createTasks(view);
|
||||
header.addItem(tasksContainer, {
|
||||
CSSStyles: {
|
||||
'width': `${maxWidth}px`,
|
||||
'height': '150px',
|
||||
}
|
||||
});
|
||||
|
||||
header.addItem(await this.createFooter(view), {
|
||||
container.addItem(await this.createFooter(view), {
|
||||
CSSStyles: {
|
||||
'margin-top': '20px'
|
||||
}
|
||||
});
|
||||
|
||||
const mainContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute'
|
||||
}).component();
|
||||
mainContainer.addItem(container, {
|
||||
CSSStyles: { 'padding-top': '25px', 'padding-left': '5px' }
|
||||
});
|
||||
await view.initializeModel(mainContainer);
|
||||
await view.initializeModel(container);
|
||||
|
||||
this.refreshMigrations();
|
||||
});
|
||||
@@ -85,12 +78,14 @@ export class DashboardWidget {
|
||||
value: loc.DASHBOARD_TITLE,
|
||||
CSSStyles: {
|
||||
'font-size': '36px',
|
||||
'margin-bottom': '5px',
|
||||
}
|
||||
}).component();
|
||||
const descComponent = view.modelBuilder.text().withProps({
|
||||
value: loc.DASHBOARD_DESCRIPTION,
|
||||
CSSStyles: {
|
||||
'font-size': '12px',
|
||||
'margin-top': '10px',
|
||||
}
|
||||
}).component();
|
||||
header.addItems([titleComponent, descComponent], {
|
||||
@@ -99,7 +94,6 @@ export class DashboardWidget {
|
||||
'padding-left': '20px'
|
||||
}
|
||||
});
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
@@ -135,7 +129,9 @@ export class DashboardWidget {
|
||||
const preRequisiteListElement = view.modelBuilder.text().withProps({
|
||||
value: points,
|
||||
CSSStyles: {
|
||||
'padding-left': '15px'
|
||||
'padding-left': '15px',
|
||||
'margin-bottom': '5px',
|
||||
'margin-top': '10px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
@@ -160,7 +156,6 @@ export class DashboardWidget {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
tasksContainer.addItem(migrateButton, {
|
||||
CSSStyles: {
|
||||
'margin-top': '20px',
|
||||
@@ -199,19 +194,172 @@ export class DashboardWidget {
|
||||
}
|
||||
|
||||
private async refreshMigrations(): Promise<void> {
|
||||
this._migrationStatusCardLoadingContainer.loading = true;
|
||||
this._migrationStatusCardsContainer.clearItems();
|
||||
const currentConnection = (await azdata.connection.getCurrentConnection());
|
||||
const getMigrations = MigrationLocalStorage.getMigrations(currentConnection);
|
||||
getMigrations.forEach((migration) => {
|
||||
const button = this._view.modelBuilder.button().withProps({
|
||||
label: `Migration to ${migration.targetManagedInstance.name} using controller ${migration.migrationContext.name}`
|
||||
}).component();
|
||||
button.onDidClick(async (e) => {
|
||||
try {
|
||||
const migrationStatus = await this.getMigrations();
|
||||
|
||||
const inProgressMigrations = migrationStatus.filter((value) => {
|
||||
const status = value.migrationContext.properties.migrationStatus;
|
||||
return status === 'InProgress' || status === 'Creating' || status === 'Completing';
|
||||
});
|
||||
const inProgressMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.inProgressMigration,
|
||||
loc.MIGRATION_IN_PROGRESS,
|
||||
loc.LOG_SHIPPING_IN_PROGRESS,
|
||||
inProgressMigrations.length
|
||||
);
|
||||
inProgressMigrationButton.onDidClick((e) => {
|
||||
const dialog = new MigrationStatusDialog(migrationStatus, MigrationCategory.ONGOING);
|
||||
dialog.initialize();
|
||||
});
|
||||
this._migrationStatusCardsContainer.addItem(inProgressMigrationButton);
|
||||
|
||||
const successfulMigration = migrationStatus.filter((value) => {
|
||||
const status = value.migrationContext.properties.migrationStatus;
|
||||
return status === 'Succeeded';
|
||||
});
|
||||
const successfulMigrationButton = this.createStatusCard(
|
||||
IconPathHelper.completedMigration,
|
||||
loc.MIGRATION_COMPLETED,
|
||||
loc.SUCCESSFULLY_MIGRATED_TO_AZURE_SQL,
|
||||
successfulMigration.length
|
||||
);
|
||||
successfulMigrationButton.onDidClick((e) => {
|
||||
const dialog = new MigrationStatusDialog(migrationStatus, MigrationCategory.SUCCEEDED);
|
||||
dialog.initialize();
|
||||
});
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
button
|
||||
successfulMigrationButton
|
||||
);
|
||||
|
||||
const currentConnection = (await azdata.connection.getCurrentConnection());
|
||||
const localMigrations = MigrationLocalStorage.getMigrationsBySourceConnections(currentConnection);
|
||||
const migrationDatabases = new Set(
|
||||
localMigrations.filter((value) => {
|
||||
|
||||
}).map((value) => {
|
||||
return value.migrationContext.properties.sourceDatabaseName;
|
||||
}));
|
||||
const serverDatabases = await azdata.connection.listDatabases(currentConnection.connectionId);
|
||||
const notStartedMigrationCard = this.createStatusCard(
|
||||
IconPathHelper.notStartedMigration,
|
||||
loc.MIGRATION_NOT_STARTED,
|
||||
loc.CHOOSE_TO_MIGRATE_TO_AZURE_SQL,
|
||||
serverDatabases.length - migrationDatabases.size
|
||||
);
|
||||
notStartedMigrationCard.onDidClick((e) => {
|
||||
vscode.window.showInformationMessage('Feature coming soon');
|
||||
});
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
notStartedMigrationCard
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
this._migrationStatusCardLoadingContainer.loading = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async getMigrations(): Promise<MigrationContext[]> {
|
||||
const currentConnection = (await azdata.connection.getCurrentConnection());
|
||||
const localMigrations = MigrationLocalStorage.getMigrationsBySourceConnections(currentConnection);
|
||||
for (let i = 0; i < localMigrations.length; i++) {
|
||||
const localMigration = localMigrations[i];
|
||||
localMigration.migrationContext = await getDatabaseMigration(
|
||||
localMigration.azureAccount,
|
||||
localMigration.subscription,
|
||||
localMigration.targetManagedInstance.location,
|
||||
localMigration.migrationContext.id
|
||||
);
|
||||
localMigration.sourceConnectionProfile = currentConnection;
|
||||
}
|
||||
return localMigrations;
|
||||
}
|
||||
|
||||
private createStatusCard(
|
||||
cardIconPath: IconPath,
|
||||
cardTitle: string,
|
||||
cardDescription: string,
|
||||
count: number
|
||||
): azdata.DivContainer {
|
||||
|
||||
const cardTitleText = this._view.modelBuilder.text().withProps({ value: cardTitle }).withProps({
|
||||
CSSStyles: {
|
||||
'font-weight': 'bold',
|
||||
'height': '20px',
|
||||
'margin-top': '6px',
|
||||
'margin-bottom': '0px',
|
||||
'width': '300px',
|
||||
'font-size': '14px'
|
||||
}
|
||||
}).component();
|
||||
const cardDescriptionText = this._view.modelBuilder.text().withProps({ value: cardDescription }).withProps({
|
||||
CSSStyles: {
|
||||
'height': '18px',
|
||||
'margin-top': '0px',
|
||||
'margin-bottom': '0px',
|
||||
'width': '300px'
|
||||
}
|
||||
}).component();
|
||||
const cardCount = this._view.modelBuilder.text().withProps({
|
||||
value: count.toString(),
|
||||
CSSStyles: {
|
||||
'font-size': '28px',
|
||||
'line-height': '36px',
|
||||
'margin-top': '4px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const flexContainer = this._view.modelBuilder.flexContainer().withItems([
|
||||
cardTitleText,
|
||||
cardDescriptionText
|
||||
]).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'width': '300px',
|
||||
'height': '50px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const flex = this._view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'width': '400px',
|
||||
'height': '50px',
|
||||
'margin-top': '10px',
|
||||
'box-shadow': '0 1px 2px 0 rgba(0,0,0,0.2)'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const img = this._view.modelBuilder.image().withProps({
|
||||
iconPath: cardIconPath!.light,
|
||||
iconHeight: 16,
|
||||
iconWidth: 16,
|
||||
width: 64,
|
||||
height: 50
|
||||
}).component();
|
||||
|
||||
flex.addItem(img, {
|
||||
flex: '0'
|
||||
});
|
||||
flex.addItem(flexContainer, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'width': '300px'
|
||||
}
|
||||
});
|
||||
flex.addItem(cardCount, {
|
||||
flex: '0'
|
||||
});
|
||||
|
||||
const compositeButton = this._view.modelBuilder.divContainer().withItems([flex]).withProps({
|
||||
ariaRole: 'button',
|
||||
ariaLabel: 'show status',
|
||||
clickable: true
|
||||
}).component();
|
||||
return compositeButton;
|
||||
}
|
||||
|
||||
private async createFooter(view: azdata.ModelView): Promise<azdata.Component> {
|
||||
@@ -258,14 +406,22 @@ export class DashboardWidget {
|
||||
|
||||
const viewAllButton = view.modelBuilder.hyperlink().withProps({
|
||||
label: loc.VIEW_ALL,
|
||||
url: ''
|
||||
url: '',
|
||||
CSSStyles: {
|
||||
'font-size': '13px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
viewAllButton.onDidClick(async (e) => {
|
||||
new MigrationStatusDialog(await this.getMigrations(), MigrationCategory.ALL).initialize();
|
||||
});
|
||||
|
||||
const refreshButton = view.modelBuilder.hyperlink().withProps({
|
||||
label: loc.REFRESH,
|
||||
url: '',
|
||||
CSSStyles: {
|
||||
'text-align': 'right'
|
||||
'text-align': 'right',
|
||||
'font-size': '13px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
@@ -305,6 +461,7 @@ export class DashboardWidget {
|
||||
|
||||
|
||||
this._migrationStatusCardsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
this._migrationStatusCardLoadingContainer = view.modelBuilder.loadingComponent().withItem(this._migrationStatusCardsContainer).component();
|
||||
|
||||
statusContainer.addItem(
|
||||
header, {
|
||||
@@ -318,7 +475,7 @@ export class DashboardWidget {
|
||||
}
|
||||
);
|
||||
|
||||
statusContainer.addItem(this._migrationStatusCardsContainer, {
|
||||
statusContainer.addItem(this._migrationStatusCardLoadingContainer, {
|
||||
CSSStyles: {
|
||||
'margin-top': '30px'
|
||||
}
|
||||
@@ -374,12 +531,12 @@ export class DashboardWidget {
|
||||
|
||||
const videosContainer = this.createVideoLinkContainers(view, [
|
||||
{
|
||||
iconPath: IconPathHelper.sqlMiImportHelpThumbnail,
|
||||
iconPath: IconPathHelper.sqlMiVideoThumbnail,
|
||||
description: loc.HELP_VIDEO1_TITLE,
|
||||
link: 'https://www.youtube.com/watch?v=sE99cSoFOHs' //TODO: Fix Video link
|
||||
},
|
||||
{
|
||||
iconPath: IconPathHelper.sqlVmImportHelpThumbnail,
|
||||
iconPath: IconPathHelper.sqlVmVideoThumbnail,
|
||||
description: loc.HELP_VIDEO2_TITLE,
|
||||
link: 'https://www.youtube.com/watch?v=R4GCBoxADyQ' //TODO: Fix video link
|
||||
}
|
||||
@@ -447,13 +604,10 @@ export class DashboardWidget {
|
||||
flexFlow: 'row',
|
||||
width: maxWidth,
|
||||
}).component();
|
||||
|
||||
links.forEach(link => {
|
||||
const videoContainer = this.createVideoLink(view, link);
|
||||
|
||||
videosContainer.addItem(videoContainer);
|
||||
});
|
||||
|
||||
return videosContainer;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as azdata from 'azdata';
|
||||
import { MigrationStateModel } from '../../models/stateMachine';
|
||||
import { SqlDatabaseTree } from './sqlDatabasesTree';
|
||||
import { SqlMigrationImpactedObjectInfo } from '../../../../mssql/src/mssql';
|
||||
import { SKURecommendationPage } from '../../wizard/skuRecommendationPage';
|
||||
|
||||
export type Issues = {
|
||||
description: string,
|
||||
@@ -30,7 +31,7 @@ export class AssessmentResultsDialog {
|
||||
private _tree: SqlDatabaseTree;
|
||||
|
||||
|
||||
constructor(public ownerUri: string, public model: MigrationStateModel, public title: string) {
|
||||
constructor(public ownerUri: string, public model: MigrationStateModel, public title: string, private skuRecommendationPage: SKURecommendationPage) {
|
||||
this._model = model;
|
||||
let assessmentData = this.parseData(this._model);
|
||||
this._tree = new SqlDatabaseTree(this._model, assessmentData);
|
||||
@@ -126,6 +127,7 @@ export class AssessmentResultsDialog {
|
||||
|
||||
protected async execute() {
|
||||
this.model._migrationDbs = this._tree.selectedDbs();
|
||||
this.skuRecommendationPage.refreshDatabaseCount(this._model._migrationDbs.length);
|
||||
this._isOpen = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { createMigrationController, getMigrationControllerRegions, getMigrationController, getResourceGroups, getMigrationControllerAuthKeys, getMigrationControllerMonitoringData } from '../../api/azure';
|
||||
import { MigrationStateModel } from '../../models/stateMachine';
|
||||
import * as constants from '../../models/strings';
|
||||
import * as constants from '../../constants/strings';
|
||||
import * as os from 'os';
|
||||
import { azureResource } from 'azureResource';
|
||||
import { IntergrationRuntimePage } from '../../wizard/integrationRuntimePage';
|
||||
@@ -130,7 +130,6 @@ export class CreateMigrationControllerDialog {
|
||||
this._dialogObject.okButton.enabled = false;
|
||||
azdata.window.openDialog(this._dialogObject);
|
||||
this._dialogObject.cancelButton.onClick((e) => {
|
||||
this.migrationStateModel._migrationController = undefined!;
|
||||
});
|
||||
this._dialogObject.okButton.onClick((e) => {
|
||||
this.irPage.populateMigrationController();
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { IconPathHelper } from '../../constants/iconPathHelper';
|
||||
import { MigrationContext } from '../../models/migrationLocalStorage';
|
||||
import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
|
||||
import * as loc from '../../constants/strings';
|
||||
export class MigrationCutoverDialog {
|
||||
private _dialogObject!: azdata.window.Dialog;
|
||||
private _view!: azdata.ModelView;
|
||||
private _model: MigrationCutoverDialogModel;
|
||||
|
||||
private _databaseTitleName!: azdata.TextComponent;
|
||||
private _databaseCutoverButton!: azdata.ButtonComponent;
|
||||
private _refresh!: azdata.ButtonComponent;
|
||||
|
||||
private _serverName!: azdata.TextComponent;
|
||||
private _serverVersion!: azdata.TextComponent;
|
||||
private _targetServer!: azdata.TextComponent;
|
||||
private _targetVersion!: azdata.TextComponent;
|
||||
private _migrationStatus!: azdata.TextComponent;
|
||||
private _fullBackupFile!: azdata.TextComponent;
|
||||
private _lastAppliedLSN!: azdata.TextComponent;
|
||||
private _lastAppliedBackupFile!: azdata.TextComponent;
|
||||
private _lastAppliedBackupTakenOn!: azdata.TextComponent;
|
||||
|
||||
private _fileCount!: azdata.TextComponent;
|
||||
|
||||
private fileTable!: azdata.TableComponent;
|
||||
|
||||
private _startCutover!: boolean;
|
||||
|
||||
constructor(migration: MigrationContext) {
|
||||
this._model = new MigrationCutoverDialogModel(migration);
|
||||
this._dialogObject = azdata.window.createModelViewDialog(loc.MIGRATION_CUTOVER, 'MigrationCutoverDialog', 1000);
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
let tab = azdata.window.createTab('');
|
||||
tab.registerContent(async (view: azdata.ModelView) => {
|
||||
this._view = view;
|
||||
const sourceDetails = this.createInfoField(loc.SOURCE_VERSION, '');
|
||||
const sourceVersion = this.createInfoField(loc.SOURCE_VERSION, '');
|
||||
|
||||
this._serverName = sourceDetails.text;
|
||||
this._serverVersion = sourceVersion.text;
|
||||
|
||||
const flexServer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
flexServer.addItem(sourceDetails.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '150px'
|
||||
}
|
||||
});
|
||||
flexServer.addItem(sourceVersion.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '150px'
|
||||
}
|
||||
});
|
||||
|
||||
const targetServer = this.createInfoField(loc.TARGET_SERVER, '');
|
||||
const targetVersion = this.createInfoField(loc.TARGET_VERSION, '');
|
||||
|
||||
this._targetServer = targetServer.text;
|
||||
this._targetVersion = targetVersion.text;
|
||||
|
||||
const flexTarget = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
flexTarget.addItem(targetServer.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '230px'
|
||||
}
|
||||
});
|
||||
flexTarget.addItem(targetVersion.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '230px'
|
||||
}
|
||||
});
|
||||
|
||||
const migrationStatus = this.createInfoField(loc.MIGRATION_STATUS, '');
|
||||
const fullBackupFileOn = this.createInfoField(loc.FULL_BACKUP_FILES, '');
|
||||
|
||||
|
||||
this._migrationStatus = migrationStatus.text;
|
||||
this._fullBackupFile = fullBackupFileOn.text;
|
||||
|
||||
const flexStatus = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
flexStatus.addItem(migrationStatus.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '180px'
|
||||
}
|
||||
});
|
||||
flexStatus.addItem(fullBackupFileOn.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '180px'
|
||||
}
|
||||
});
|
||||
|
||||
const lastSSN = this.createInfoField(loc.LAST_APPLIED_LSN, '');
|
||||
const lastAppliedBackup = this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
|
||||
const lastAppliedBackupOn = this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '');
|
||||
|
||||
this._lastAppliedLSN = lastSSN.text;
|
||||
this._lastAppliedBackupFile = lastAppliedBackup.text;
|
||||
this._lastAppliedBackupTakenOn = lastAppliedBackupOn.text;
|
||||
|
||||
const flexFile = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
flexFile.addItem(lastSSN.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '230px'
|
||||
}
|
||||
});
|
||||
flexFile.addItem(lastAppliedBackup.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '230px'
|
||||
}
|
||||
});
|
||||
flexFile.addItem(lastAppliedBackupOn.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '230px'
|
||||
}
|
||||
});
|
||||
const flexInfo = view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'width': '700px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
flexInfo.addItem(flexServer, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': '150px'
|
||||
}
|
||||
});
|
||||
|
||||
flexInfo.addItem(flexTarget, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': '230px'
|
||||
}
|
||||
});
|
||||
|
||||
flexInfo.addItem(flexStatus, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': '180px'
|
||||
}
|
||||
});
|
||||
|
||||
flexInfo.addItem(flexFile, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': '200px'
|
||||
}
|
||||
});
|
||||
|
||||
this._fileCount = view.modelBuilder.text().withProps({
|
||||
width: '500px',
|
||||
CSSStyles: {
|
||||
'font-size': '14px',
|
||||
'font-weight': 'bold'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this.fileTable = view.modelBuilder.table().withProps({
|
||||
columns: [
|
||||
{
|
||||
value: loc.ACTIVE_BACKUP_FILES,
|
||||
width: 150,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.TYPE,
|
||||
width: 100,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.STATUS,
|
||||
width: 100,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.BACKUP_START_TIME,
|
||||
width: 150,
|
||||
type: azdata.ColumnType.text
|
||||
}, {
|
||||
value: loc.FIRST_LSN,
|
||||
width: 150,
|
||||
type: azdata.ColumnType.text
|
||||
}, {
|
||||
value: loc.LAST_LSN,
|
||||
width: 150,
|
||||
type: azdata.ColumnType.text
|
||||
}
|
||||
],
|
||||
data: [],
|
||||
width: '800px',
|
||||
height: '600px',
|
||||
}).component();
|
||||
|
||||
const formBuilder = view.modelBuilder.formContainer().withFormItems(
|
||||
[
|
||||
{
|
||||
component: await this.migrationContainerHeader()
|
||||
},
|
||||
{
|
||||
component: flexInfo
|
||||
},
|
||||
{
|
||||
component: this._fileCount
|
||||
},
|
||||
{
|
||||
component: this.fileTable
|
||||
}
|
||||
],
|
||||
{
|
||||
horizontal: false
|
||||
}
|
||||
);
|
||||
const form = formBuilder.withLayout({ width: '100%' }).component();
|
||||
return view.initializeModel(form);
|
||||
});
|
||||
this._dialogObject.content = [tab];
|
||||
azdata.window.openDialog(this._dialogObject);
|
||||
this.refreshStatus();
|
||||
}
|
||||
|
||||
|
||||
private migrationContainerHeader(): azdata.FlexContainer {
|
||||
const header = this._view.modelBuilder.flexContainer().withLayout({
|
||||
}).component();
|
||||
|
||||
this._databaseTitleName = this._view.modelBuilder.text().withProps({
|
||||
CSSStyles: {
|
||||
'font-size': 'large',
|
||||
'width': '400px'
|
||||
},
|
||||
value: this._model._migration.migrationContext.name
|
||||
}).component();
|
||||
|
||||
header.addItem(this._databaseTitleName, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'width': '500px'
|
||||
}
|
||||
});
|
||||
|
||||
this._databaseCutoverButton = this._view.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.cutover,
|
||||
iconHeight: '14px',
|
||||
iconWidth: '12px',
|
||||
label: 'Start Cutover',
|
||||
height: '55px',
|
||||
width: '100px',
|
||||
enabled: false
|
||||
}).component();
|
||||
|
||||
this._databaseCutoverButton.onDidClick(async (e) => {
|
||||
if (this._startCutover) {
|
||||
await this._model.startCutover();
|
||||
this.refreshStatus();
|
||||
} else {
|
||||
this._dialogObject.message = {
|
||||
text: loc.CANNOT_START_CUTOVER_ERROR,
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
header.addItem(this._databaseCutoverButton, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'width': '100px'
|
||||
}
|
||||
});
|
||||
|
||||
this._refresh = this._view.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.refresh,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
label: 'Refresh',
|
||||
height: '55px',
|
||||
width: '100px'
|
||||
}).component();
|
||||
|
||||
this._refresh.onDidClick((e) => {
|
||||
this.refreshStatus();
|
||||
});
|
||||
|
||||
header.addItem(this._refresh, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'width': '100px'
|
||||
}
|
||||
});
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
|
||||
private async refreshStatus(): Promise<void> {
|
||||
try {
|
||||
await this._model.fetchStatus();
|
||||
const sqlServerInfo = await azdata.connection.getServerInfo(this._model._migration.sourceConnectionProfile.connectionId);
|
||||
const sqlServerName = this._model._migration.sourceConnectionProfile.serverName;
|
||||
const sqlServerVersion = sqlServerInfo.serverVersion;
|
||||
const sqlServerEdition = sqlServerInfo.serverEdition;
|
||||
const targetServerName = this._model._migration.targetManagedInstance.name;
|
||||
let targetServerVersion;
|
||||
if (this._model.migrationStatus.id.includes('managedInstances')) {
|
||||
targetServerVersion = loc.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
|
||||
} else {
|
||||
targetServerVersion = loc.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
|
||||
}
|
||||
|
||||
const migrationStatusTextValue = this._model.migrationStatus.properties.migrationStatus;
|
||||
|
||||
let fullBackupFileName: string;
|
||||
let lastAppliedSSN: string;
|
||||
let lastAppliedBackupFileTakenOn: string;
|
||||
|
||||
|
||||
const tableData: ActiveBackupFileSchema[] = [];
|
||||
|
||||
this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach((activeBackupSet) => {
|
||||
tableData.push(
|
||||
{
|
||||
fileName: activeBackupSet.listOfBackupFiles[0].fileName,
|
||||
type: activeBackupSet.backupType,
|
||||
status: activeBackupSet.listOfBackupFiles[0].status,
|
||||
backupStartTime: activeBackupSet.backupStartDate,
|
||||
firstLSN: activeBackupSet.firstLSN,
|
||||
lastLSN: activeBackupSet.lastLSN
|
||||
}
|
||||
);
|
||||
if (activeBackupSet.listOfBackupFiles[0].fileName.substr(activeBackupSet.listOfBackupFiles[0].fileName.lastIndexOf('.') + 1) === 'bak') {
|
||||
fullBackupFileName = activeBackupSet.listOfBackupFiles[0].fileName;
|
||||
}
|
||||
if (activeBackupSet.listOfBackupFiles[0].fileName === this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
|
||||
lastAppliedSSN = activeBackupSet.lastLSN;
|
||||
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
|
||||
}
|
||||
});
|
||||
|
||||
this._serverName.value = sqlServerName;
|
||||
this._serverVersion.value = `${sqlServerVersion}
|
||||
${sqlServerEdition}`;
|
||||
|
||||
this._targetServer.value = targetServerName;
|
||||
this._targetVersion.value = targetServerVersion;
|
||||
|
||||
this._migrationStatus.value = migrationStatusTextValue;
|
||||
this._fullBackupFile.value = fullBackupFileName!;
|
||||
|
||||
this._lastAppliedLSN.value = lastAppliedSSN!;
|
||||
this._lastAppliedBackupFile.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename;
|
||||
this._lastAppliedBackupTakenOn.value = new Date(lastAppliedBackupFileTakenOn!).toLocaleString();
|
||||
|
||||
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
|
||||
|
||||
this.fileTable.data = tableData.map((row) => {
|
||||
return [
|
||||
row.fileName,
|
||||
row.type,
|
||||
row.status,
|
||||
new Date(row.backupStartTime).toLocaleString(),
|
||||
row.firstLSN,
|
||||
row.lastLSN
|
||||
];
|
||||
});
|
||||
if (this._model.migrationStatus.properties.migrationStatusDetails?.isFullBackupRestored) {
|
||||
this._startCutover = true;
|
||||
}
|
||||
|
||||
if (migrationStatusTextValue === 'InProgress') {
|
||||
this._databaseCutoverButton.enabled = true;
|
||||
} else {
|
||||
this._databaseCutoverButton.enabled = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
private createInfoField(label: string, value: string): {
|
||||
flexContainer: azdata.FlexContainer,
|
||||
text: azdata.TextComponent
|
||||
} {
|
||||
const flexContainer = this._view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
const labelComponent = this._view.modelBuilder.text().withProps({
|
||||
value: label,
|
||||
CSSStyles: {
|
||||
'font-weight': 'bold',
|
||||
'margin-bottom': '0'
|
||||
}
|
||||
}).component();
|
||||
flexContainer.addItem(labelComponent);
|
||||
|
||||
const textComponent = this._view.modelBuilder.text().withProps({
|
||||
value: value,
|
||||
CSSStyles: {
|
||||
'margin-top': '5px',
|
||||
'margin-bottom': '0'
|
||||
}
|
||||
}).component();
|
||||
flexContainer.addItem(textComponent);
|
||||
return {
|
||||
flexContainer: flexContainer,
|
||||
text: textComponent
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface ActiveBackupFileSchema {
|
||||
fileName: string,
|
||||
type: string,
|
||||
status: string,
|
||||
backupStartTime: string,
|
||||
firstLSN: string,
|
||||
lastLSN: string
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { getMigrationStatus, DatabaseMigration, startMigrationCutover } from '../../api/azure';
|
||||
import { MigrationContext } from '../../models/migrationLocalStorage';
|
||||
|
||||
|
||||
export class MigrationCutoverDialogModel {
|
||||
|
||||
public migrationStatus!: DatabaseMigration;
|
||||
|
||||
constructor(public _migration: MigrationContext) {
|
||||
}
|
||||
|
||||
public async fetchStatus(): Promise<void> {
|
||||
this.migrationStatus = (await getMigrationStatus(
|
||||
this._migration.azureAccount,
|
||||
this._migration.subscription,
|
||||
this._migration.migrationContext
|
||||
));
|
||||
}
|
||||
|
||||
public async startCutover(): Promise<DatabaseMigration | undefined> {
|
||||
try {
|
||||
if (this.migrationStatus) {
|
||||
return await startMigrationCutover(
|
||||
this._migration.azureAccount,
|
||||
this._migration.subscription,
|
||||
this.migrationStatus
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
return undefined!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { IconPathHelper } from '../../constants/iconPathHelper';
|
||||
import { MigrationContext } from '../../models/migrationLocalStorage';
|
||||
import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDialog';
|
||||
import { MigrationCategory, MigrationStatusDialogModel } from './migrationStatusDialogModel';
|
||||
import * as loc from '../../constants/strings';
|
||||
export class MigrationStatusDialog {
|
||||
private _model: MigrationStatusDialogModel;
|
||||
private _dialogObject!: azdata.window.Dialog;
|
||||
private _view!: azdata.ModelView;
|
||||
private _searchBox!: azdata.InputBoxComponent;
|
||||
private _refresh!: azdata.ButtonComponent;
|
||||
private _statusDropdown!: azdata.DropDownComponent;
|
||||
private _statusTable!: azdata.DeclarativeTableComponent;
|
||||
|
||||
constructor(migrations: MigrationContext[], private _filter: MigrationCategory) {
|
||||
this._model = new MigrationStatusDialogModel(migrations);
|
||||
this._dialogObject = azdata.window.createModelViewDialog(loc.MIGRATION_STATUS, 'MigrationControllerDialog', 'wide');
|
||||
}
|
||||
|
||||
initialize() {
|
||||
let tab = azdata.window.createTab('');
|
||||
tab.registerContent((view: azdata.ModelView) => {
|
||||
this._view = view;
|
||||
|
||||
this._statusDropdown = this._view.modelBuilder.dropDown().withProps({
|
||||
values: this._model.statusDropdownValues,
|
||||
width: '220px'
|
||||
}).component();
|
||||
|
||||
this._statusDropdown.onValueChanged((value) => {
|
||||
this.populateMigrationTable();
|
||||
});
|
||||
|
||||
this._statusDropdown.value = this._statusDropdown.values![this._filter];
|
||||
|
||||
const formBuilder = view.modelBuilder.formContainer().withFormItems(
|
||||
[
|
||||
{
|
||||
component: this.createSearchAndRefreshContainer()
|
||||
},
|
||||
{
|
||||
component: this._statusDropdown
|
||||
},
|
||||
{
|
||||
component: this.createStatusTable()
|
||||
}
|
||||
],
|
||||
{
|
||||
horizontal: false
|
||||
}
|
||||
);
|
||||
const form = formBuilder.withLayout({ width: '100%' }).component();
|
||||
return view.initializeModel(form);
|
||||
});
|
||||
this._dialogObject.content = [tab];
|
||||
azdata.window.openDialog(this._dialogObject);
|
||||
}
|
||||
|
||||
private createSearchAndRefreshContainer(): azdata.FlexContainer {
|
||||
this._searchBox = this._view.modelBuilder.inputBox().withProps({
|
||||
placeHolder: loc.SEARCH_FOR_MIGRATIONS,
|
||||
width: '360px'
|
||||
}).component();
|
||||
|
||||
this._searchBox.onTextChanged((value) => {
|
||||
this.populateMigrationTable();
|
||||
});
|
||||
|
||||
this._refresh = this._view.modelBuilder.button().withProps({
|
||||
iconPath: {
|
||||
light: IconPathHelper.refresh.light,
|
||||
dark: IconPathHelper.refresh.dark
|
||||
},
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
height: '30px',
|
||||
label: 'Refresh',
|
||||
}).component();
|
||||
|
||||
const flexContainer = this._view.modelBuilder.flexContainer().component();
|
||||
|
||||
flexContainer.addItem(this._searchBox, {
|
||||
flex: '0'
|
||||
});
|
||||
|
||||
flexContainer.addItem(this._refresh, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'margin-left': '20px'
|
||||
}
|
||||
});
|
||||
|
||||
return flexContainer;
|
||||
}
|
||||
|
||||
private populateMigrationTable(): void {
|
||||
|
||||
try {
|
||||
const migrations = this._model.filterMigration(
|
||||
this._searchBox.value!,
|
||||
(<azdata.CategoryValue>this._statusDropdown.value).name
|
||||
);
|
||||
|
||||
const data: azdata.DeclarativeTableCellValue[][] = [];
|
||||
|
||||
migrations.forEach((migration) => {
|
||||
const migrationRow: azdata.DeclarativeTableCellValue[] = [];
|
||||
|
||||
const databaseHyperLink = this._view.modelBuilder.hyperlink().withProps({
|
||||
label: migration.migrationContext.name,
|
||||
url: ''
|
||||
}).component();
|
||||
databaseHyperLink.onDidClick(async (e) => {
|
||||
await (new MigrationCutoverDialog(migration)).initialize();
|
||||
});
|
||||
migrationRow.push({
|
||||
value: databaseHyperLink,
|
||||
});
|
||||
|
||||
migrationRow.push({
|
||||
value: migration.migrationContext.properties.migrationStatus
|
||||
});
|
||||
|
||||
const sqlMigrationIcon = this._view.modelBuilder.image().withProps({
|
||||
iconPath: IconPathHelper.sqlMigrationLogo,
|
||||
iconWidth: '16px',
|
||||
iconHeight: '16px',
|
||||
width: '32px',
|
||||
height: '20px'
|
||||
}).component();
|
||||
const sqlMigrationName = this._view.modelBuilder.hyperlink().withProps({
|
||||
label: migration.migrationContext.name,
|
||||
url: ''
|
||||
}).component();
|
||||
sqlMigrationName.onDidClick((e) => {
|
||||
vscode.window.showInformationMessage('Feature coming soon');
|
||||
});
|
||||
|
||||
const sqlMigrationContainer = this._view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'justify-content': 'center'
|
||||
}
|
||||
}).component();
|
||||
sqlMigrationContainer.addItem(sqlMigrationIcon, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'width': '32px'
|
||||
}
|
||||
});
|
||||
sqlMigrationContainer.addItem(sqlMigrationName,
|
||||
{
|
||||
CSSStyles: {
|
||||
'width': 'auto'
|
||||
}
|
||||
});
|
||||
migrationRow.push({
|
||||
value: sqlMigrationContainer
|
||||
});
|
||||
|
||||
migrationRow.push({
|
||||
value: loc.ONLINE
|
||||
});
|
||||
|
||||
migrationRow.push({
|
||||
value: '---'
|
||||
});
|
||||
migrationRow.push({
|
||||
value: '---'
|
||||
});
|
||||
|
||||
data.push(migrationRow);
|
||||
});
|
||||
|
||||
this._statusTable.dataValues = data;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
private createStatusTable(): azdata.DeclarativeTableComponent {
|
||||
this._statusTable = this._view.modelBuilder.declarativeTable().withProps({
|
||||
columns: [
|
||||
{
|
||||
displayName: loc.DATABASE,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
width: '100px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: {
|
||||
'text-align': 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: loc.MIGRATION_STATUS,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '150px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: {
|
||||
'text-align': 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: loc.TARGET_AZURE_SQL_INSTANCE_NAME,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
width: '300px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: {
|
||||
'text-align': 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: loc.CUTOVER_TYPE,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '100px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: {
|
||||
'text-align': 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: loc.START_TIME,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '150px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: {
|
||||
'text-align': 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: loc.FINISH_TIME,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '150px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: {
|
||||
'text-align': 'center'
|
||||
}
|
||||
}
|
||||
]
|
||||
}).component();
|
||||
return this._statusTable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { MigrationContext } from '../../models/migrationLocalStorage';
|
||||
|
||||
export class MigrationStatusDialogModel {
|
||||
|
||||
public statusDropdownValues: azdata.CategoryValue[] = [
|
||||
{
|
||||
displayName: 'Status: All',
|
||||
name: 'All',
|
||||
}, {
|
||||
displayName: 'Status: Ongoing',
|
||||
name: 'Ongoing',
|
||||
}, {
|
||||
displayName: 'Status: Succeeded',
|
||||
name: 'Succeeded',
|
||||
}
|
||||
];
|
||||
|
||||
constructor(public _migrations: MigrationContext[]) {
|
||||
}
|
||||
|
||||
public filterMigration(databaseName: string, category: string): MigrationContext[] {
|
||||
let filteredMigration: MigrationContext[] = [];
|
||||
if (category === 'All') {
|
||||
filteredMigration = this._migrations;
|
||||
} else if (category === 'Ongoing') {
|
||||
filteredMigration = this._migrations.filter((value) => {
|
||||
const status = value.migrationContext.properties.migrationStatus;
|
||||
return status === 'InProgress' || status === 'Creating' || status === 'Completing';
|
||||
});
|
||||
} else if (category === 'Succeeded') {
|
||||
filteredMigration = this._migrations.filter((value) => {
|
||||
const status = value.migrationContext.properties.migrationStatus;
|
||||
return status === 'Succeeded';
|
||||
});
|
||||
}
|
||||
if (databaseName) {
|
||||
filteredMigration = filteredMigration.filter((value) => {
|
||||
return value.migrationContext.name.toLowerCase().includes(databaseName.toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
return filteredMigration;
|
||||
}
|
||||
}
|
||||
|
||||
export enum MigrationCategory {
|
||||
ALL,
|
||||
ONGOING,
|
||||
SUCCEEDED
|
||||
}
|
||||
@@ -6,9 +6,8 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import { WizardController } from './wizard/wizardController';
|
||||
import { AssessmentResultsDialog } from './dialog/assessmentResults/assessmentResultsDialog';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as loc from './models/strings';
|
||||
import * as loc from './constants/strings';
|
||||
import { MigrationNotebookInfo, NotebookPathHelper } from './constants/notebookPathHelper';
|
||||
import { IconPathHelper } from './constants/iconPathHelper';
|
||||
import { DashboardWidget } from './dashboard/sqlServerDashboard';
|
||||
@@ -42,12 +41,6 @@ class SQLMigration {
|
||||
const wizardController = new WizardController(this.context);
|
||||
await wizardController.openWizard(connectionId);
|
||||
}),
|
||||
|
||||
vscode.commands.registerCommand('sqlmigration.testDialog', async () => {
|
||||
let dialog = new AssessmentResultsDialog('ownerUri', undefined!, 'Assessment Dialog');
|
||||
await dialog.openDialog();
|
||||
}),
|
||||
|
||||
vscode.commands.registerCommand('sqlmigration.openNotebooks', async () => {
|
||||
const input = vscode.window.createQuickPick<MigrationNotebookInfo>();
|
||||
input.placeholder = loc.NOTEBOOK_QUICK_PICK_PLACEHOLDER;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as vscode from 'vscode';
|
||||
import { azureResource } from 'azureResource';
|
||||
import { DatabaseMigration, MigrationController, SqlManagedInstance } from '../api/azure';
|
||||
import { DatabaseMigration, SqlMigrationController, SqlManagedInstance } from '../api/azure';
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export class MigrationLocalStorage {
|
||||
MigrationLocalStorage.context = context;
|
||||
}
|
||||
|
||||
public static getMigrations(connectionProfile: azdata.connection.ConnectionProfile): MigrationContext[] {
|
||||
public static getMigrationsBySourceConnections(connectionProfile: azdata.connection.ConnectionProfile): MigrationContext[] {
|
||||
|
||||
let dataBaseMigrations: MigrationContext[] = [];
|
||||
try {
|
||||
@@ -41,7 +41,7 @@ export class MigrationLocalStorage {
|
||||
targetMI: SqlManagedInstance,
|
||||
azureAccount: azdata.Account,
|
||||
subscription: azureResource.AzureResourceSubscription,
|
||||
controller: MigrationController): void {
|
||||
controller: SqlMigrationController): void {
|
||||
try {
|
||||
const migrationMementos: MigrationContext[] = this.context.globalState.get(this.mementoToken) || [];
|
||||
migrationMementos.push({
|
||||
@@ -69,5 +69,5 @@ export interface MigrationContext {
|
||||
targetManagedInstance: SqlManagedInstance,
|
||||
azureAccount: azdata.Account,
|
||||
subscription: azureResource.AzureResourceSubscription,
|
||||
controller: MigrationController
|
||||
controller: SqlMigrationController
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import * as azdata from 'azdata';
|
||||
import { azureResource } from 'azureResource';
|
||||
import * as vscode from 'vscode';
|
||||
import * as mssql from '../../../mssql';
|
||||
import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getMigrationControllers, getSubscriptions, MigrationController, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount } from '../api/azure';
|
||||
import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getMigrationControllers, getSubscriptions, SqlMigrationController, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount } from '../api/azure';
|
||||
import { SKURecommendations } from './externalContract';
|
||||
import * as constants from '../models/strings';
|
||||
import * as constants from '../constants/strings';
|
||||
import { MigrationLocalStorage } from './migrationLocalStorage';
|
||||
|
||||
export enum State {
|
||||
@@ -85,13 +85,13 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
public _targetManagedInstance!: SqlManagedInstance;
|
||||
|
||||
public _databaseBackup!: DatabaseBackupModel;
|
||||
public _migrationDbs!: string[];
|
||||
public _migrationDbs: string[] = [];
|
||||
public _storageAccounts!: StorageAccount[];
|
||||
public _fileShares!: azureResource.FileShare[];
|
||||
public _blobContainers!: azureResource.BlobContainer[];
|
||||
|
||||
public _migrationController!: MigrationController;
|
||||
public _migrationControllers!: MigrationController[];
|
||||
public _migrationController!: SqlMigrationController;
|
||||
public _migrationControllers!: SqlMigrationController[];
|
||||
public _nodeNames!: string[];
|
||||
|
||||
private _stateChangeEventEmitter = new vscode.EventEmitter<StateChangeEvent>();
|
||||
@@ -403,7 +403,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
return migrationControllerValues;
|
||||
}
|
||||
|
||||
public getMigrationController(index: number): MigrationController {
|
||||
public getMigrationController(index: number): SqlMigrationController {
|
||||
return this._migrationControllers[index];
|
||||
}
|
||||
|
||||
@@ -421,7 +421,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
const requestBody: StartDatabaseMigrationRequest = {
|
||||
location: this._migrationController?.properties.location!,
|
||||
properties: {
|
||||
SourceDatabaseName: currentConnection?.databaseName!,
|
||||
SourceDatabaseName: '',
|
||||
MigrationController: this._migrationController?.id!,
|
||||
BackupConfiguration: {
|
||||
TargetLocation: {
|
||||
@@ -445,26 +445,36 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
}
|
||||
};
|
||||
|
||||
const response = await startDatabaseMigration(
|
||||
this._azureAccount,
|
||||
this._targetSubscription,
|
||||
this._targetManagedInstance.resourceGroup!,
|
||||
this._migrationController?.properties.location!,
|
||||
this._targetManagedInstance.name,
|
||||
this._migrationController?.name!,
|
||||
requestBody
|
||||
);
|
||||
this._migrationDbs.forEach(async (db) => {
|
||||
|
||||
if (response.status === 201) {
|
||||
MigrationLocalStorage.saveMigration(
|
||||
currentConnection!,
|
||||
response.databaseMigration,
|
||||
this._targetManagedInstance,
|
||||
this._azureAccount,
|
||||
this._targetSubscription,
|
||||
this._migrationController
|
||||
);
|
||||
}
|
||||
requestBody.properties.SourceDatabaseName = db;
|
||||
try {
|
||||
const response = await startDatabaseMigration(
|
||||
this._azureAccount,
|
||||
this._targetSubscription,
|
||||
this._targetManagedInstance.resourceGroup!,
|
||||
this._migrationController?.properties.location!,
|
||||
this._targetManagedInstance.name,
|
||||
currentConnection?.databaseName!,
|
||||
requestBody
|
||||
);
|
||||
|
||||
if (response.status === 201) {
|
||||
MigrationLocalStorage.saveMigration(
|
||||
currentConnection!,
|
||||
response.databaseMigration,
|
||||
this._targetManagedInstance,
|
||||
this._azureAccount,
|
||||
this._targetSubscription,
|
||||
this._migrationController
|
||||
);
|
||||
vscode.window.showInformationMessage(`Starting migration for database ${db} to ${this._targetManagedInstance.name}`);
|
||||
}
|
||||
} catch (e) {
|
||||
vscode.window.showInformationMessage(e);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
vscode.window.showInformationMessage(constants.MIGRATION_STARTED);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
|
||||
import * as constants from '../models/strings';
|
||||
import * as constants from '../constants/strings';
|
||||
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
|
||||
|
||||
export class AccountsSelectionPage extends MigrationWizardPage {
|
||||
|
||||
@@ -8,8 +8,8 @@ import { EOL } from 'os';
|
||||
import { getStorageAccountAccessKeys } from '../api/azure';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationCutover, MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
|
||||
import * as constants from '../models/strings';
|
||||
|
||||
import * as constants from '../constants/strings';
|
||||
import * as vscode from 'vscode';
|
||||
export class DatabaseBackupPage extends MigrationWizardPage {
|
||||
|
||||
private _networkShareContainer!: azdata.FlexContainer;
|
||||
@@ -85,7 +85,9 @@ export class DatabaseBackupPage extends MigrationWizardPage {
|
||||
|
||||
blobContainerButton.onDidChangeCheckedState((e) => {
|
||||
if (e) {
|
||||
this.toggleNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER);
|
||||
vscode.window.showInformationMessage('Feature coming soon');
|
||||
networkShareButton.checked = true;
|
||||
//this.toggleNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -97,7 +99,9 @@ export class DatabaseBackupPage extends MigrationWizardPage {
|
||||
|
||||
fileShareButton.onDidChangeCheckedState((e) => {
|
||||
if (e) {
|
||||
this.toggleNetworkContainerFields(NetworkContainerType.FILE_SHARE);
|
||||
vscode.window.showInformationMessage('Feature coming soon');
|
||||
networkShareButton.checked = true;
|
||||
//this.toggleNetworkContainerFields(NetworkContainerType.FILE_SHARE);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -414,7 +418,9 @@ export class DatabaseBackupPage extends MigrationWizardPage {
|
||||
|
||||
offlineButton.onDidChangeCheckedState((e) => {
|
||||
if (e) {
|
||||
this.migrationStateModel._databaseBackup.migrationCutover = MigrationCutover.OFFLINE;
|
||||
vscode.window.showInformationMessage('Feature coming soon');
|
||||
onlineButton.checked = true;
|
||||
//this.migrationStateModel._databaseBackup.migrationCutover = MigrationCutover.OFFLINE;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -489,7 +495,9 @@ export class DatabaseBackupPage extends MigrationWizardPage {
|
||||
|
||||
public async onPageLeave(): Promise<void> {
|
||||
this.migrationStateModel._databaseBackup.storageKey = (await getStorageAccountAccessKeys(this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.storageAccount)).keyName1;
|
||||
console.log(this.migrationStateModel._databaseBackup);
|
||||
this.wizard.registerNavigationValidator((pageChangeInfo) => {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as vscode from 'vscode';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
|
||||
import { CreateMigrationControllerDialog } from '../dialog/createMigrationDialog/createMigrationControllerDialog';
|
||||
import * as constants from '../models/strings';
|
||||
import * as constants from '../constants/strings';
|
||||
import { createInformationRow, WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
|
||||
import { getMigrationController, getMigrationControllerAuthKeys, getMigrationControllerMonitoringData } from '../api/azure';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
@@ -147,7 +147,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
|
||||
return flexContainer;
|
||||
}
|
||||
|
||||
public async populateMigrationController(controllerStatus?: string): Promise<void> {
|
||||
public async populateMigrationController(): Promise<void> {
|
||||
this.migrationControllerDropdown.loading = true;
|
||||
try {
|
||||
this.migrationControllerDropdown.values = await this.migrationStateModel.getMigrationControllerValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._targetManagedInstance);
|
||||
|
||||
@@ -8,11 +8,10 @@ import * as path from 'path';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
|
||||
import { Product, ProductLookupTable } from '../models/product';
|
||||
import { Disposable } from 'vscode';
|
||||
import { AssessmentResultsDialog } from '../dialog/assessmentResults/assessmentResultsDialog';
|
||||
import { getAvailableManagedInstanceProducts, getSubscriptions, SqlManagedInstance, Subscription } from '../api/azure';
|
||||
import * as constants from '../models/strings';
|
||||
import { azureResource } from 'azureResource';
|
||||
import * as constants from '../constants/strings';
|
||||
import * as vscode from 'vscode';
|
||||
import { EOL } from 'os';
|
||||
|
||||
// import { SqlMigrationService } from '../../../../extensions/mssql/src/sqlMigration/sqlMigrationService';
|
||||
|
||||
@@ -32,11 +31,11 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
private _azureSubscriptionText: azdata.FormComponent<azdata.TextComponent> | undefined;
|
||||
private _managedInstanceSubscriptionDropdown!: azdata.DropDownComponent;
|
||||
private _managedInstanceDropdown!: azdata.DropDownComponent;
|
||||
private _subscriptionDropdownValues: azdata.CategoryValue[] = [];
|
||||
private _subscriptionMap: Map<string, Subscription> = new Map();
|
||||
private _view: azdata.ModelView | undefined;
|
||||
private _rbg!: azdata.RadioCardGroupComponent;
|
||||
|
||||
private async initialState(view: azdata.ModelView) {
|
||||
this._view = view;
|
||||
this._igComponent = this.createStatusComponent(view); // The first component giving basic information
|
||||
this._detailsComponent = this.createDetailsComponent(view); // The details of what can be moved
|
||||
this._chooseTargetComponent = this.createChooseTargetComponent(view);
|
||||
@@ -47,12 +46,24 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
}).component();
|
||||
this._managedInstanceSubscriptionDropdown = view.modelBuilder.dropDown().component();
|
||||
this._managedInstanceSubscriptionDropdown.onValueChanged((e) => {
|
||||
this.populateManagedInstanceDropdown();
|
||||
if (e.selected) {
|
||||
this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(e.index);
|
||||
this.migrationStateModel._targetManagedInstance = undefined!;
|
||||
this.migrationStateModel._migrationController = undefined!;
|
||||
this.populateManagedInstanceDropdown();
|
||||
}
|
||||
});
|
||||
const managedInstanceDropdownLabel = view.modelBuilder.text().withProps({
|
||||
value: constants.MANAGED_INSTANCE
|
||||
}).component();
|
||||
|
||||
this._managedInstanceDropdown = view.modelBuilder.dropDown().component();
|
||||
this._managedInstanceDropdown.onValueChanged((e) => {
|
||||
if (e.selected) {
|
||||
this.migrationStateModel._migrationControllers = undefined!;
|
||||
this.migrationStateModel._targetManagedInstance = this.migrationStateModel.getManagedInstance(e.index);
|
||||
}
|
||||
});
|
||||
|
||||
const targetContainer = view.modelBuilder.flexContainer().withItems(
|
||||
[
|
||||
@@ -137,18 +148,23 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
private constructTargets(): void {
|
||||
const products: Product[] = Object.values(ProductLookupTable);
|
||||
|
||||
const rbg = this._view!.modelBuilder.radioCardGroup().withProperties<azdata.RadioCardGroupComponentProperties>({
|
||||
this._rbg = this._view!.modelBuilder.radioCardGroup().withProperties<azdata.RadioCardGroupComponentProperties>({
|
||||
cards: [],
|
||||
cardWidth: '600px',
|
||||
cardHeight: '60px',
|
||||
orientation: azdata.Orientation.Vertical,
|
||||
iconHeight: '30px',
|
||||
iconWidth: '30px'
|
||||
});
|
||||
}).component();
|
||||
|
||||
products.forEach((product) => {
|
||||
const imagePath = path.resolve(this.migrationStateModel.getExtensionPath(), 'media', product.icon ?? 'ads.svg');
|
||||
|
||||
let dbCount = 0;
|
||||
if (product.type === 'AzureSQLVM') {
|
||||
dbCount = 0;
|
||||
} else {
|
||||
dbCount = this.migrationStateModel._migrationDbs.length;
|
||||
}
|
||||
const descriptions: azdata.RadioCardDescription[] = [
|
||||
{
|
||||
textValue: product.name,
|
||||
@@ -164,7 +180,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
}
|
||||
},
|
||||
{
|
||||
textValue: '9 databases will be migrated',
|
||||
textValue: `${dbCount} databases will be migrated`,
|
||||
linkDisplayValue: 'View/Change',
|
||||
displayLinkCodicon: true,
|
||||
linkCodiconStyles: {
|
||||
@@ -174,22 +190,31 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
}
|
||||
];
|
||||
|
||||
rbg.component().cards.push({
|
||||
id: product.name,
|
||||
this._rbg.cards.push({
|
||||
id: product.type,
|
||||
icon: imagePath,
|
||||
descriptions
|
||||
});
|
||||
});
|
||||
|
||||
rbg.component().onLinkClick(async (value) => {
|
||||
this._rbg.onLinkClick(async (value) => {
|
||||
|
||||
//check which card is being selected, and open correct dialog based on link
|
||||
console.log(value);
|
||||
let dialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, 'Assessment Dialog');
|
||||
let dialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, 'Assessment Dialog', this);
|
||||
await dialog.openDialog();
|
||||
});
|
||||
|
||||
this._chooseTargetComponent?.component.addItem(rbg.component());
|
||||
this._rbg.onSelectionChanged((value) => {
|
||||
if (value.cardId === 'AzureSQLVM') {
|
||||
vscode.window.showInformationMessage('Feature coming soon');
|
||||
this._rbg.selectedCardId = 'AzureSQLMI';
|
||||
}
|
||||
});
|
||||
|
||||
this._rbg.selectedCardId = 'AzureSQLMI';
|
||||
|
||||
this._chooseTargetComponent?.component.addItem(this._rbg);
|
||||
}
|
||||
|
||||
private createAzureSubscriptionText(view: azdata.ModelView): azdata.FormComponent<azdata.TextComponent> {
|
||||
@@ -205,85 +230,78 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
}
|
||||
|
||||
private async populateSubscriptionDropdown(): Promise<void> {
|
||||
this._managedInstanceSubscriptionDropdown.loading = true;
|
||||
this._managedInstanceDropdown.loading = true;
|
||||
let subscriptions: azureResource.AzureResourceSubscription[] = [];
|
||||
try {
|
||||
subscriptions = await getSubscriptions(this.migrationStateModel._azureAccount);
|
||||
subscriptions.forEach((subscription) => {
|
||||
this._subscriptionMap.set(subscription.id, subscription);
|
||||
this._subscriptionDropdownValues.push({
|
||||
name: subscription.id,
|
||||
displayName: subscription.name + ' - ' + subscription.id,
|
||||
});
|
||||
});
|
||||
|
||||
if (!this._subscriptionDropdownValues || this._subscriptionDropdownValues.length === 0) {
|
||||
this._subscriptionDropdownValues = [
|
||||
{
|
||||
displayName: constants.NO_SUBSCRIPTIONS_FOUND,
|
||||
name: ''
|
||||
}
|
||||
];
|
||||
if (!this.migrationStateModel._targetSubscription) {
|
||||
this._managedInstanceSubscriptionDropdown.loading = true;
|
||||
this._managedInstanceDropdown.loading = true;
|
||||
try {
|
||||
this._managedInstanceSubscriptionDropdown.values = await this.migrationStateModel.getSubscriptionsDropdownValues();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
this._managedInstanceSubscriptionDropdown.loading = false;
|
||||
}
|
||||
|
||||
this._managedInstanceSubscriptionDropdown.values = this._subscriptionDropdownValues;
|
||||
} catch (error) {
|
||||
this.setEmptyDropdownPlaceHolder(this._managedInstanceSubscriptionDropdown, constants.NO_SUBSCRIPTIONS_FOUND);
|
||||
this._managedInstanceDropdown.loading = false;
|
||||
}
|
||||
this.populateManagedInstanceDropdown();
|
||||
this._managedInstanceSubscriptionDropdown.loading = false;
|
||||
}
|
||||
|
||||
private async populateManagedInstanceDropdown(): Promise<void> {
|
||||
this._managedInstanceDropdown.loading = true;
|
||||
let mis: SqlManagedInstance[] = [];
|
||||
let miValues: azdata.CategoryValue[] = [];
|
||||
try {
|
||||
const subscriptionId = (<azdata.CategoryValue>this._managedInstanceSubscriptionDropdown.value).name;
|
||||
|
||||
mis = await getAvailableManagedInstanceProducts(this.migrationStateModel._azureAccount, this._subscriptionMap.get(subscriptionId)!);
|
||||
mis.forEach((mi) => {
|
||||
miValues.push({
|
||||
name: mi.name,
|
||||
displayName: mi.name
|
||||
});
|
||||
});
|
||||
|
||||
if (!miValues || miValues.length === 0) {
|
||||
miValues = [
|
||||
{
|
||||
displayName: constants.NO_MANAGED_INSTANCE_FOUND,
|
||||
name: ''
|
||||
}
|
||||
];
|
||||
if (!this.migrationStateModel._targetManagedInstance) {
|
||||
this._managedInstanceDropdown.loading = true;
|
||||
try {
|
||||
this._managedInstanceDropdown.values = await this.migrationStateModel.getManagedInstanceValues(this.migrationStateModel._targetSubscription);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
this._managedInstanceDropdown.loading = false;
|
||||
}
|
||||
|
||||
this._managedInstanceDropdown.values = miValues;
|
||||
} catch (error) {
|
||||
this.setEmptyDropdownPlaceHolder(this._managedInstanceDropdown, constants.NO_MANAGED_INSTANCE_FOUND);
|
||||
}
|
||||
|
||||
this._managedInstanceDropdown.loading = false;
|
||||
}
|
||||
|
||||
private setEmptyDropdownPlaceHolder(dropDown: azdata.DropDownComponent, placeholder: string): void {
|
||||
dropDown.values = [{
|
||||
displayName: placeholder,
|
||||
name: ''
|
||||
}];
|
||||
}
|
||||
|
||||
private eventListener: Disposable | undefined;
|
||||
private eventListener: vscode.Disposable | undefined;
|
||||
public async onPageEnter(): Promise<void> {
|
||||
this.eventListener = this.migrationStateModel.stateChangeEvent(async (e) => this.onStateChangeEvent(e));
|
||||
this.populateSubscriptionDropdown();
|
||||
this.constructDetails();
|
||||
|
||||
this.wizard.registerNavigationValidator((pageChangeInfo) => {
|
||||
const errors: string[] = [];
|
||||
this.wizard.message = {
|
||||
text: '',
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
|
||||
return true;
|
||||
}
|
||||
if (this.migrationStateModel._migrationDbs.length === 0) {
|
||||
errors.push('Please select databases to migrate');
|
||||
|
||||
}
|
||||
if ((<azdata.CategoryValue>this._managedInstanceSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
|
||||
errors.push(constants.INVALID_SUBSCRIPTION_ERROR);
|
||||
}
|
||||
if ((<azdata.CategoryValue>this._managedInstanceDropdown.value).displayName === constants.NO_MANAGED_INSTANCE_FOUND) {
|
||||
errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
this.wizard.message = {
|
||||
text: errors.join(EOL),
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public async onPageLeave(): Promise<void> {
|
||||
this.eventListener?.dispose();
|
||||
this.wizard.message = {
|
||||
text: '',
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
this.wizard.registerNavigationValidator((pageChangeInfo) => {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
@@ -292,4 +310,16 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
}
|
||||
}
|
||||
|
||||
public refreshDatabaseCount(count: number): void {
|
||||
this.wizard.message = {
|
||||
text: '',
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
const textValue: string = `${count} databases will be migrated`;
|
||||
this._rbg.cards[0].descriptions[1].textValue = textValue;
|
||||
this._rbg.updateProperties({
|
||||
cards: this._rbg.cards
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { SOURCE_CONFIGURATION_PAGE_TITLE, COLLECTING_SOURCE_CONFIGURATIONS, COLLECTING_SOURCE_CONFIGURATIONS_INFO, COLLECTING_SOURCE_CONFIGURATIONS_ERROR } from '../models/strings';
|
||||
import { SOURCE_CONFIGURATION_PAGE_TITLE, COLLECTING_SOURCE_CONFIGURATIONS, COLLECTING_SOURCE_CONFIGURATIONS_INFO, COLLECTING_SOURCE_CONFIGURATIONS_ERROR } from '../constants/strings';
|
||||
import { MigrationStateModel, StateChangeEvent, State } from '../models/stateMachine';
|
||||
import { Disposable } from 'vscode';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import * as azdata from 'azdata';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
|
||||
import { SUBSCRIPTION_SELECTION_PAGE_TITLE, SUBSCRIPTION_SELECTION_AZURE_ACCOUNT_TITLE, SUBSCRIPTION_SELECTION_AZURE_PRODUCT_TITLE, SUBSCRIPTION_SELECTION_AZURE_SUBSCRIPTION_TITLE } from '../models/strings';
|
||||
import { SUBSCRIPTION_SELECTION_PAGE_TITLE, SUBSCRIPTION_SELECTION_AZURE_ACCOUNT_TITLE, SUBSCRIPTION_SELECTION_AZURE_PRODUCT_TITLE, SUBSCRIPTION_SELECTION_AZURE_SUBSCRIPTION_TITLE } from '../constants/strings';
|
||||
import { Disposable } from 'vscode';
|
||||
import { getSubscriptions, Subscription, getAvailableManagedInstanceProducts, AzureProduct, getAvailableSqlServers } from '../api/azure';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import * as azdata from 'azdata';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
|
||||
import * as constants from '../models/strings';
|
||||
import * as constants from '../constants/strings';
|
||||
import { createHeadingTextComponent, createInformationRow } from './wizardController';
|
||||
|
||||
export class SummaryPage extends MigrationWizardPage {
|
||||
@@ -42,7 +42,7 @@ export class SummaryPage extends MigrationWizardPage {
|
||||
createInformationRow(this._view, constants.TYPE, constants.SUMMARY_MI_TYPE),
|
||||
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name),
|
||||
createInformationRow(this._view, constants.SUMMARY_MI_TYPE, this.migrationStateModel._targetManagedInstance.name),
|
||||
createInformationRow(this._view, constants.SUMMARY_DATABASE_COUNT_LABEL, '1'),
|
||||
createInformationRow(this._view, constants.SUMMARY_DATABASE_COUNT_LABEL, this.migrationStateModel._migrationDbs.length.toString()),
|
||||
createHeadingTextComponent(this._view, constants.DATABASE_BACKUP_PAGE_TITLE),
|
||||
this.createNetworkContainerRows(),
|
||||
createHeadingTextComponent(this._view, constants.IR_PAGE_TITLE),
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import * as azdata from 'azdata';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
|
||||
import * as constants from '../models/strings';
|
||||
import * as constants from '../constants/strings';
|
||||
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
|
||||
|
||||
export class TempTargetSelectionPage extends MigrationWizardPage {
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as mssql from '../../../mssql';
|
||||
import { MigrationStateModel } from '../models/stateMachine';
|
||||
import { WIZARD_TITLE } from '../models/strings';
|
||||
import * as loc from '../constants/strings';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { SKURecommendationPage } from './skuRecommendationPage';
|
||||
// import { SubscriptionSelectionPage } from './subscriptionSelectionPage';
|
||||
@@ -31,7 +31,7 @@ export class WizardController {
|
||||
}
|
||||
|
||||
private async createWizard(stateModel: MigrationStateModel): Promise<void> {
|
||||
const wizard = azdata.window.createWizard(WIZARD_TITLE, 'wide');
|
||||
const wizard = azdata.window.createWizard(loc.WIZARD_TITLE, 'wide');
|
||||
wizard.generateScriptButton.enabled = false;
|
||||
wizard.generateScriptButton.hidden = true;
|
||||
const skuRecommendationPage = new SKURecommendationPage(wizard, stateModel);
|
||||
|
||||