mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-14 01:25:37 -05:00
[Feature] SKU recommendations in SQL migration extension (#18252)
* Initial check in for SQL migration SKU recommendation feature (#18116) Co-authored-by: Raymond Truong <ratruong@microsoft.com> * add TargetSelectionPage, remove AccountSelectionPage, fix saveAndClose bugs (#18092) * update sku interfaces (#18150) * create the skuRecommendationResultsDialog (#18151) * add TargetSelectionPage, remove AccountSelectionPage, fix saveAndClose bugs * create skuRecommendationResultsDialog * Replace placeholder SKU recommendation results with actual results (#18153) * Replace placeholder SKU recommendation results with actual backend call results * Remove skuRecommendationExample * Replace number fields in interfaces with correct enums, and update UI text * add getAzureRecommendationDialog for performance collection (#18159) * add getAzureRecommendationDialog when there are no recommendations available * update 'get azure rec' / 'view details' link values * add condition to check if recommendations are available * Implement start/stop perf data collection + import perf data into new UI (#18149) * Implement start/stop perf data collection * add getAzureRecommendationDialog when there are no recommendations available * update 'get azure rec' / 'view details' link values * add condition to check if recommendations are available * Implement import existing data + start/stop perf collection with new UI Co-authored-by: Rachel Kim <rackim@microsoft.com> * Expose SqlInstanceRequirements in SKU recommendation results (#18207) * Expose SqlInstanceRequirements * Move string literals to constants file * Fix formatting in mssql.d.ts * create storage properties table (#18215) * Edit sku recommendation parameters (#18244) * Edit sku recommendation parameters * make _targetPercentileDropdown not editable; styling updates * Azure recommendation section exposes data collection status and stop option (#18246) * Edit sku recommendation parameters * create azure recommendation details section on sku page * Improve error handling + add auto refresh + other small changes (#18228) * Update source properties table * WIP - refresh perf data collection * Add auto refresh logic * Address comments Co-authored-by: Rachel Kim <rackim@microsoft.com> * Show/hide azure recommendation components based on data collection source and status (#18254) * add refresh recommendation button; show/hide content based on perf collection status * show/hide azure rec content based on perf data source scenarios * add popups for start/stop; allow user to restart data collection; add perf collection to save close; add info tooltips (#18278) * Update SKU recommendation timer logic (#18281) * Update timer logic * Fix misc UI bugs * update sql migration extension readme (#18295) * Remove empty constant, as this may have broken the build * Fix 'save and close' behavior for SKU recommendation (#18301) * Update timer logic * Fix misc UI bugs * 'WIP' * Add logic to restore correct SKU recommendation state when reloading * SKU UX enhancements - status info, button validations, savedInfo logic (#18320) * add stop/inprogress status icons to perf collection status text, update restart icon * refactor savedInfo as an interface, edit parameter recommednations are saved, add open folder inputbox validation, handle no recommendations available scenario * fix getazureredialog bug, cleanup cold * nit card styling * Update recommendations whenever user changes list of databases to assess + misc clean up (#18323) * Consolidate constants, clean up redundant functions, misc clean up * Remove old SKU recommendation interfaces * Update some more strings * Telemetry events for SKU Recommendation (#18282) * Telemetry events for SKU Recommendation * Addressing comments - 1) fixed camel casing 2) removed extra logging to console 3) added telemetry for subid, rg, tenantid on targetselectionpage * Resolving conflicts * Addressing comments - 1) removing filename 2) moving all numbers to measurements. * Resolving comment - Fixing telemetry value for tenant id. * removing warning 'logError' is declared but its value is never read (#18333) * Stop existing data collection when leaving and starting a new migration + update timers (#18339) * Refresh recommendations when pressing stop data collection button * Fix orphaned data collection when save and closing and starting a new migration * Revert "Refresh recommendations when pressing stop data collection button" This reverts commit e6fb2ade8f8a41952adb81cb0ee852414dfa4ef2. * Update timers to use production values * Remove unused import * address bug bash issues: add learn more link, add last refreshed time, fix vm card view detail open issue, remember last selected folder, remove strings, refactor refresh logic on sku page (#18340) * Address comments * Update to sqltoolsservice 3.0.0-release.204 Co-authored-by: Rachel Kim <rackim@microsoft.com> Co-authored-by: Neetu Singh <23.neetu@gmail.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
|
||||
"version": "3.0.0-release.202",
|
||||
"version": "3.0.0-release.204",
|
||||
"downloadFileNames": {
|
||||
"Windows_86": "win-x86-net6.0.zip",
|
||||
"Windows_64": "win-x64-net6.0.zip",
|
||||
|
||||
1
extensions/mssql/src/mssql.d.ts
vendored
1
extensions/mssql/src/mssql.d.ts
vendored
@@ -885,6 +885,7 @@ export interface StopPerfDataCollectionResult {
|
||||
}
|
||||
|
||||
export interface RefreshPerfDataCollectionResult {
|
||||
isCollecting: boolean;
|
||||
messages: string[];
|
||||
errors: string[];
|
||||
refreshTime: Date;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Azure SQL Migration
|
||||
The Azure SQL Migration extension in Azure Data Studio brings together a simplified assessment and migration experience that delivers the following capabilities:
|
||||
- A responsive user interface that provides an easy-to-navigate step-by-step wizard to deliver an integrated assessment and migration experience.
|
||||
- A responsive user interface that provides an easy-to-navigate step-by-step wizard to deliver an integrated assessment, Azure recommendation and migration experience.
|
||||
- An enhanced assessment engine that can assess SQL Server instances and identify databases that are ready for migration to Azure SQL Managed Instance or SQL Server on Azure Virtual Machines.
|
||||
- SKU recommender to collect performance data from the source SQL Server instance to generate right-sized Azure SQL recommendation.
|
||||
- A reliable Azure service powered by Azure Database Migration service that orchestrates data movement activities to deliver a seamless migration experience with minimal downtime.
|
||||
- The ability to run migrations in either online (for migrations that require minimal downtime) or offline (for migrations where downtime persists through the duration of the migration) modes to suit your business requirements.
|
||||
- The flexibility to create and configure a self-hosted integration runtime to provide your own compute for access to source SQL Server and backups in your on-premises environment.
|
||||
@@ -13,15 +14,20 @@ From Azure Data Studio marketplace, install the latest version of “Azure SQL M
|
||||
|
||||
|
||||
## Things you need before starting Azure SQL migration
|
||||
- an Azure account
|
||||
- an Azure account for migration (not required for assessment or SKU recommendation features)
|
||||
- an Azure SQL Managed Instance or SQL Server on Azure Virtual Machine to migrate your database(s) to
|
||||
- your database backup location details
|
||||
|
||||
## Getting started
|
||||
Refer to [Migrate databases using the Azure SQL Migration extension for Azure Data Studio](https://docs.microsoft.com/azure/dms/migration-using-azure-data-studio) for detailed documentation on capabilities and concepts.
|
||||
|
||||
## Assessment and SKU recommendation
|
||||
The assessment and SKU recommendation feature
|
||||
- evaluates the source SQL Server database(s) for migration readiness.
|
||||
- generates right-sized SKU recommendations in Azure to meet the performance requirements of the source database(s) with minimal cost. [Learn more.](https://aka.ms/ads-sql-sku-recommend)
|
||||
|
||||
## Azure SQL targets
|
||||
The Azure SQL Migration extension supports database target readiness assessments and migrations to the following Azure SQL targets.
|
||||
The Azure SQL Migration extension supports database migrations to the following Azure SQL targets.
|
||||
- [SQL on Azure Virtual Machines (Windows)](https://docs.microsoft.com/azure/azure-sql/virtual-machines/windows/sql-server-on-azure-vm-iaas-what-is-overview)
|
||||
- [Azure SQL Managed Instance](https://docs.microsoft.com/azure/azure-sql/managed-instance/sql-managed-instance-paas-overview)
|
||||
|
||||
|
||||
3
extensions/sql-migration/images/edit.svg
Normal file
3
extensions/sql-migration/images/edit.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 2.62C15.9975 2.96301 15.9296 3.3024 15.8 3.62C15.6698 3.94212 15.476 4.23464 15.23 4.48L5 14.77L0 16L1.23 11.05L11.52 0.77C11.7654 0.524034 12.0579 0.330158 12.38 0.2C12.6976 0.0704162 13.037 0.00253902 13.38 0C13.7242 0.000976246 14.0645 0.0724452 14.38 0.21C14.6919 0.338295 14.9743 0.528845 15.21 0.77C15.4479 1.00841 15.638 1.29014 15.77 1.6C15.9171 1.92018 15.9955 2.26766 16 2.62ZM1.38 14.62L4 14C3.94126 13.7636 3.85395 13.5353 3.74 13.32C3.62299 13.1141 3.48201 12.9228 3.32 12.75C3.14724 12.588 2.95591 12.447 2.75 12.33C2.51363 12.2036 2.26139 12.1094 2 12.05L1.38 14.62ZM2.55 11.16C3.0852 11.3342 3.57163 11.6324 3.96961 12.0304C4.36759 12.4284 4.66583 12.9148 4.84 13.45L13.29 5L11 2.71L2.55 11.16ZM14 4.29L14.38 3.92C14.4943 3.80702 14.6012 3.68677 14.7 3.56C14.789 3.42875 14.8628 3.28786 14.92 3.14C14.9755 2.97236 15.0026 2.79659 15 2.62C15.0032 2.40625 14.9588 2.19445 14.87 2C14.7902 1.80451 14.6711 1.62751 14.52 1.48C14.3725 1.32892 14.1955 1.20978 14 1.13C13.8055 1.04118 13.5938 0.996774 13.38 1C13.2034 0.997418 13.0276 1.02446 12.86 1.08C12.7121 1.13724 12.5712 1.21105 12.44 1.3C12.3108 1.39589 12.1904 1.50297 12.08 1.62L11.71 2L14 4.29Z" fill="#0078D4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -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 |
4
extensions/sql-migration/images/stop.svg
Normal file
4
extensions/sql-migration/images/stop.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<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="#7A7A7A"/>
|
||||
<path d="M11 5H5V11H11V5Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 281 B |
@@ -247,3 +247,7 @@ export function clearDialogMessage(dialog: window.Dialog): void {
|
||||
text: ''
|
||||
};
|
||||
}
|
||||
|
||||
export function getUserHome(): string | undefined {
|
||||
return process.env.HOME || process.env.USERPROFILE;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ export class IconPathHelper {
|
||||
public static emptyTable: IconPath;
|
||||
public static addAzureAccount: IconPath;
|
||||
public static retry: IconPath;
|
||||
public static edit: IconPath;
|
||||
public static restartDataCollection: IconPath;
|
||||
public static stop: IconPath;
|
||||
|
||||
public static setExtensionContext(context: vscode.ExtensionContext) {
|
||||
IconPathHelper.copy = {
|
||||
@@ -158,5 +161,17 @@ export class IconPathHelper {
|
||||
light: context.asAbsolutePath('images/retry.svg'),
|
||||
dark: context.asAbsolutePath('images/retry.svg')
|
||||
};
|
||||
IconPathHelper.edit = {
|
||||
light: context.asAbsolutePath('images/edit.svg'),
|
||||
dark: context.asAbsolutePath('images/edit.svg')
|
||||
};
|
||||
IconPathHelper.restartDataCollection = {
|
||||
light: context.asAbsolutePath('images/restartDataCollection.svg'),
|
||||
dark: context.asAbsolutePath('images/restartDataCollection.svg')
|
||||
};
|
||||
IconPathHelper.stop = {
|
||||
light: context.asAbsolutePath('images/stop.svg'),
|
||||
dark: context.asAbsolutePath('images/stop.svg')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,18 +22,26 @@ export const RESUME_TITLE = localize('sql.migration.resume.title', "Run migratio
|
||||
export const START_MIGRATION = localize('sql.migration.resume.start', "Start with migration assessment again (recommended)");
|
||||
export const CONTINUE_MIGRATION = localize('sql.migration.resume.continue', "Continue last migration attempt...");
|
||||
|
||||
// Assessments Progress Page
|
||||
// Databases for assessment
|
||||
export const DATABASE_FOR_ASSESSMENT_PAGE_TITLE = localize('sql.migration.database.assessment.title', "Databases for assessment");
|
||||
export const DATABASE_FOR_ASSESSMENT_DESCRIPTION = localize('sql.migration.database.assessment.description', "Select the databases that you want to assess for migration to Azure SQL.");
|
||||
|
||||
// Assessment results and recommendations
|
||||
export const ASSESSMENT_RESULTS_AND_RECOMMENDATIONS_PAGE_TITLE = localize('sql.migration.assessment.results.and.recommendations.title', "Assessment results and recommendations");
|
||||
export const ASSESSMENT_BLOCKING_ISSUE_TITLE = localize('sql.migration.assessments.blocking.issue', 'This is a blocking issue that will prevent the database migration from succeeding.');
|
||||
export const ASSESSMENT_IN_PROGRESS = localize('sql.migration.assessment.in.progress', "Assessment in progress");
|
||||
export function ASSESSMENT_IN_PROGRESS_CONTENT(dbName: string) {
|
||||
return localize('sql.migration.assessment.in.progress.content', "We are assessing the databases in your SQL Server instance {0} to identify the right Azure SQL target.\n\nThis may take some time.", dbName);
|
||||
}
|
||||
|
||||
export const SKU_RECOMMENDATION_PAGE_TITLE = localize('sql.migration.wizard.sku.title', "Azure SQL target");
|
||||
export const SKU_RECOMMENDATION_ALL_SUCCESSFUL = (databaseCount: number): string => {
|
||||
return localize('sql.migration.wizard.sku.all', "Based on the assessment results, all {0} of your databases in an online state can be migrated to Azure SQL.", databaseCount);
|
||||
};
|
||||
export const SKU_RECOMMENDATION_ERROR = localize('sql.migration.wizard.sku.error', "An error occurred while assessing your databases.");
|
||||
export const SKU_RECOMMENDATION_ERROR = (serverName: string): string => {
|
||||
return localize('sql.migration.wizard.sku.error', "An error occurred while generating SKU recommendations for the server '{0}'.", serverName);
|
||||
};
|
||||
export const SKU_RECOMMENDATION_NO_RECOMMENDATION = localize('sql.migration.wizard.sku.error.noRecommendation', 'No recommendation available');
|
||||
export const SKU_RECOMMENDATION_NO_RECOMMENDATION_REASON = localize('sql.migration.wizard.sku.error.noRecommendation.reason', 'No SKU recommendations were generated, as there were no SKUs which could satisfy the performance characteristics of your source. Try selecting a different target platform, adjusting recommendation parameters, or selecting a different set of databases to assess.');
|
||||
export const SKU_RECOMMENDATION_ASSESSMENT_ERROR = (serverName: string): string => {
|
||||
return localize('sql.migration.wizard.sku.assessment.error', "An error occurred while assessing the server '{0}'.", serverName);
|
||||
};
|
||||
@@ -46,15 +54,17 @@ export const SKU_RECOMMENDATION_ASSESSMENT_UNEXPECTED_ERROR = (serverName: strin
|
||||
error.stack,
|
||||
EOL);
|
||||
};
|
||||
export const PERF_DATA_COLLECTION_ERROR = (serverName: string, errors: string[]): string => {
|
||||
return localize('sql.migration.wizard.perfCollection.error', "Error(s) occurred while collecting performance data for the server '{0}'. If these issues persist, try restarting the data collection process:\n\n{1}", serverName, errors.join('\n'));
|
||||
};
|
||||
export const SKU_RECOMMENDATION_ASSESSMENT_ERROR_BYPASS = localize('sql.migration.wizard.sku.assessment.error.bypass', 'Check this option to skip assessment and continue the migration.');
|
||||
export const SKU_RECOMMENDATION_ASSESSMENT_ERROR_DETAIL = localize('sql.migration.wizard.sku.assessment.error.detail', '[There are no assessment results to validate readiness of your database migration. By checking this box, you acknowledge you want to proceed migrating your database to the desired Azure SQL target.]',);
|
||||
export const REFRESH_ASSESSMENT_BUTTON_LABEL = localize('sql.migration.refresh.assessment.button.label', "Refresh assessment");
|
||||
export const SKU_RECOMMENDATION_CHOOSE_A_TARGET = localize('sql.migration.wizard.sku.choose_a_target', "Choose your Azure SQL target");
|
||||
export const SKU_RECOMMENDATION_SUBSCRIPTION_INFO = localize('sql.migration.sku.subscription', "Subscription name for your Azure SQL target");
|
||||
export const SKU_RECOMMENDATION_LOCATION_INFO = localize('sql.migration.sku.location', "Azure region for your Azure SQL target");
|
||||
export const SKU_RECOMMENDATION_RESOURCE_GROUP_INFO = localize('sql.migration.sku.resource_group', "Resource group for your Azure SQL target");
|
||||
export const SKU_RECOMMENDATION_RESOURCE_INFO = localize('sql.migration.sku.resource', "Your Azure SQL target resource name");
|
||||
|
||||
|
||||
export const SKU_RECOMMENDATION_MI_CARD_TEXT = localize('sql.migration.sku.mi.card.title', "Azure SQL Managed Instance (PaaS)");
|
||||
export const SKU_RECOMMENDATION_DB_CARD_TEXT = localize('sql.migration.sku.db.card.title', "Azure SQL Database (PaaS)");
|
||||
export const SKU_RECOMMENDATION_VM_CARD_TEXT = localize('sql.migration.sku.vm.card.title', "SQL Server on Azure Virtual Machine (IaaS)");
|
||||
export const SELECT_AZURE_MI = localize('sql.migration.select.azure.mi', "Select your target Azure subscription and your target Azure SQL Managed Instance.");
|
||||
export const SELECT_AZURE_VM = localize('sql.migration.select.azure.vm', "Select your target Azure Subscription and your target SQL Server on Azure Virtual Machine for your target.");
|
||||
@@ -62,26 +72,193 @@ export const SKU_RECOMMENDATION_VIEW_ASSESSMENT_MI = localize('sql.migration.sku
|
||||
export const SKU_RECOMMENDATION_VIEW_ASSESSMENT_VM = localize('sql.migration.sku.recommendation.view.assessment.vm', "To migrate to SQL Server on Azure Virtual Machine (IaaS), view assessment results and select one or more databases.");
|
||||
export const VIEW_SELECT_BUTTON_LABEL = localize('sql.migration.view.select.button.label', "View/Select");
|
||||
export function TOTAL_DATABASES_SELECTED(selectedDbCount: number, totalDbCount: number): string {
|
||||
return localize('total.databases.selected', "{0} of {1} databases selected.", selectedDbCount, totalDbCount);
|
||||
return localize('total.databases.selected', "{0} of {1} databases selected", selectedDbCount, totalDbCount);
|
||||
}
|
||||
export const SELECT_TARGET_TO_CONTINUE = localize('sql.migration.select.target.to.continue', "To continue, select a target database.");
|
||||
export const SELECT_TARGET_TO_CONTINUE = localize('sql.migration.select.target.to.continue', "To continue, select a target.");
|
||||
export const SELECT_DATABASE_TO_MIGRATE = localize('sql.migration.select.database.to.migrate', "Select the databases to migrate.");
|
||||
export const ASSESSMENT_COMPLETED = (serverName: string): string => {
|
||||
return localize('sql.migration.generic.congratulations', "We have completed the assessment of your SQL Server instance '{0}'.", serverName);
|
||||
};
|
||||
export const ASSESSMENT_FAILED = (serverName: string): string => {
|
||||
return localize('sql.migration.asessment.failed', "The assessment of your SQL Server instance '{0}' failed.", serverName);
|
||||
return localize('sql.migration.assessment.failed', "The assessment of your SQL Server instance '{0}' failed.", serverName);
|
||||
};
|
||||
export function ASSESSMENT_TILE(serverName: string): string {
|
||||
return localize('sql.migration.assessment', "Assessment results for '{0}'", serverName);
|
||||
}
|
||||
export function CAN_BE_MIGRATED(eligibleDbs: number, totalDbs: number): string {
|
||||
return localize('sql.migration.can.be.migrated', "{0} out of {1} databases can be migrated", eligibleDbs, totalDbs);
|
||||
return localize('sql.migration.can.be.migrated', "{0}/{1} databases can be migrated", eligibleDbs, totalDbs);
|
||||
}
|
||||
export const ASSESSMENT_MIGRATION_WARNING = localize('sql.migration.assessment.migration.warning', "Databases that are not ready for migration to Azure SQL Managed Instance can be migrated to SQL Server on Azure Virtual Machines.");
|
||||
export const DATABASES_TABLE_TILE = localize('sql.migration.databases.table.title', "Databases");
|
||||
export const SQL_SERVER_INSTANCE = localize('sql.migration.sql.server.instance', "SQL Server instance");
|
||||
|
||||
// SKU
|
||||
export const AZURE_RECOMMENDATION = localize('sql.migration.sku.recommendation', "Azure recommendation");
|
||||
export function RECOMMENDATIONS_TITLE(targetType: string): string {
|
||||
return localize('sql.migration.sku.recommendations.title', "{0} Recommendations", targetType);
|
||||
}
|
||||
export const RECOMMENDED_CONFIGURATION = localize('sql.migration.sku.recommendedConfiguration', "Recommended configuration");
|
||||
export const GET_AZURE_RECOMMENDATION = localize('sql.migration.sku.get.recommendation', "Get Azure recommendation");
|
||||
export const REFINE_AZURE_RECOMMENDATION = localize('sql.migration.sku.refine.recommendation', "Refine Azure recommendation");
|
||||
export const REFRESH_AZURE_RECOMMENDATION = localize('sql.migration.sku.refresh.recommendation', "Refresh recommendation");
|
||||
export const STOP_PERFORMANCE_COLLECTION = localize('sql.migration.sku.stop.performance.collection', "Stop data collection");
|
||||
export const RESTART_PERFORMANCE_COLLECTION = localize('sql.migration.sku.restart.performance.collection', "Restart data collection");
|
||||
export const AZURE_RECOMMENDATION_CARD_NOT_ENABLED = localize('sql.migration.sku.card.azureRecommendation.notEnabled', "Azure recommendation is not available. Click “Get Azure recommendation” button below");
|
||||
export const AZURE_RECOMMENDATION_CARD_IN_PROGRESS = localize('sql.migration.sku.card.azureRecommendation.inProgress', "Azure recommendation will be displayed once data collection is complete.");
|
||||
export const AZURE_RECOMMENDATION_STATUS_NOT_ENABLED = localize('sql.migration.sku.azureRecommendation.status.notEnabled', "Azure recommendation collects and analyzes performance data and then recommends an appropriate sized database in Azure for your workload.");
|
||||
export const AZURE_RECOMMENDATION_STATUS_IN_PROGRESS = localize('sql.migration.sku.azureRecommendation.status.inProgress', "Data collection in progress. Generating initial recommendations...");
|
||||
export const AZURE_RECOMMENDATION_STATUS_REFINING = localize('sql.migration.sku.azureRecommendation.status.refining', "Data collection still in progress. Refining existing recommendations...");
|
||||
export const AZURE_RECOMMENDATION_STATUS_STOPPED = localize('sql.migration.sku.azureRecommendation.status.stopped', "Data collection for Azure recommendations has been stopped.");
|
||||
export function AZURE_RECOMMENDATION_STATUS_AUTO_REFRESH_TIMER(mins: number): string {
|
||||
return localize('sql.migration.sku.azureRecommendation.status.autoRefreshTimer', "Initial recommendations will automatically refresh in approximately {0} minute(s).", mins.toFixed(0));
|
||||
}
|
||||
export const AZURE_RECOMMENDATION_STATUS_MANUAL_REFRESH_TIMER = localize('sql.migration.sku.azureRecommendation.status.manualRefreshTimer', "Check back periodically for updated recommendations by pressing the 'Refresh recommendation' button.");
|
||||
export const AZURE_RECOMMENDATION_STATUS_DATA_IMPORTED = localize('sql.migration.sku.azureRecommendation.status.imported', "Azure recommendation has been applied using the provided data. Import or collect additional data to refine the recommendation.");
|
||||
export const AZURE_RECOMMENDATION_TOOLTIP_NOT_STARTED = localize('sql.migration.sku.azureRecommendation.tooltip.notStarted', "Click the button below to import or collect database performance data.");
|
||||
export const AZURE_RECOMMENDATION_TOOLTIP_IN_PROGRESS = localize('sql.migration.sku.azureRecommendation.tooltip.inProgress', "Running the performance collection for a longer period of time helps ensure a more accurate recommendation.");
|
||||
|
||||
export const AZURE_RECOMMENDATION_START = localize('sql.migration.sku.azureRecommendation.start', "Start");
|
||||
export const AZURE_RECOMMENDATION_START_POPUP = localize('sql.migration.sku.azureRecommendation.start.popup', "Starting performance data collection...");
|
||||
export const AZURE_RECOMMENDATION_OPEN_EXISTING_POPUP = localize('sql.migration.sku.azureRecommendation.openExisting.popup', "Generating Azure recommendations using provided performance data...");
|
||||
export const AZURE_RECOMMENDATION_STOP_POPUP = localize('sql.migration.sku.azureRecommendation.stop.popup', "Stopping performance data collection...");
|
||||
export const AZURE_RECOMMENDATION_DESCRIPTION = localize('sql.migration.sku.azureRecommendation.description', "Azure recommendation requires performance data of SQL server instance to provide target recommendation. Enable performance data collection to receive the target recommendation for the databases you want to migrate. The longer this will be enabled the better the recommendation. You can disable performance data collection at any time.");
|
||||
export const AZURE_RECOMMENDATION_DESCRIPTION2 = localize('sql.migration.sku.azureRecommendation.description2', "You can also choose to select this data from an existing folder, if you have already collected it previously.");
|
||||
export const AZURE_RECOMMENDATION_CHOOSE_METHOD = localize('sql.migration.sku.azureRecommendation.chooseMethod.instructions', "Choose how you want to provide performance data");
|
||||
export const AZURE_RECOMMENDATION_COLLECT_DATA = localize('sql.migration.sku.azureRecommendation.collectData.method', "Collect performance data now");
|
||||
export const AZURE_RECOMMENDATION_OPEN_EXISTING = localize('sql.migration.sku.azureRecommendation.openExisting.method', "I already have the performance data");
|
||||
export const AZURE_RECOMMENDATION_COLLECT_DATA_FOLDER = localize('sql.migration.sku.azureRecommendation.collectDataSelectFolder.instructions', "Select a folder on your local drive where performance data will be saved");
|
||||
export const AZURE_RECOMMENDATION_OPEN_EXISTING_FOLDER = localize('sql.migration.sku.azureRecommendation.openExistingSelectFolder.instructions', "Select a folder on your local drive where previously collected performance data was saved");
|
||||
export const FOLDER_NAME = localize('sql.migration.azureRecommendation.folder.name', "Folder name");
|
||||
export const BROWSE = localize('sql.migration.azureRecommendation.browse', "Browse");
|
||||
export const OPEN = localize('sql.migration.azureRecommendation.open', "Open");
|
||||
|
||||
export const VIEW_DETAILS = localize('sql.migration.sku.viewDetails', "View details");
|
||||
export function ASSESSED_DBS(totalDbs: number): string {
|
||||
return localize('sql.migration.assessed.databases', "(for {0} assessed databases)", totalDbs);
|
||||
}
|
||||
export function RECOMMENDATIONS_AVAILABLE(totalDbs: number): string {
|
||||
return localize('sql.migration.sku.available.recommendations', "{0} recommendations available", totalDbs);
|
||||
}
|
||||
export const RECOMMENDATIONS = localize('sql.migration.sku.recommendations', "Recommendations");
|
||||
export const LOADING_RECOMMENDATIONS = localize('sql.migration.sku.recommendations.loading', "Loading...");
|
||||
export const TARGET_DEPLOYMENT_TYPE = localize('sql.migration.sku.targetDeploymentType', "Target deployment type");
|
||||
export const AZURE_CONFIGURATION = localize('sql.migration.sku.azureConfiguration', "Azure configuration");
|
||||
export function VM_CONFIGURATION(vmSize: string, vCPU: number): string {
|
||||
return localize('sql.migration.sku.azureConfiguration.vm', "{0} ({1} vCPU)", vmSize, vCPU);
|
||||
}
|
||||
export function VM_CONFIGURATION_PREVIEW(dataDisk: string, logDisk: string, temp: string): string {
|
||||
return localize('sql.migration.sku.azureConfiguration.vmPreview', "Data: {0}, Log: {1}, tempdb: {2}", dataDisk, logDisk, temp);
|
||||
}
|
||||
export function DB_CONFIGURATION(computeTier: string, vCore: number): string {
|
||||
return localize('sql.migration.sku.azureConfiguration.db', "{0} - {1} vCore", computeTier, vCore);
|
||||
}
|
||||
export function MI_CONFIGURATION(hardwareType: string, computeTier: string, vCore: number): string {
|
||||
return localize('sql.migration.sku.azureConfiguration.mi', "{0} - {1} - {2} vCore", hardwareType, computeTier, vCore);
|
||||
}
|
||||
export function MI_CONFIGURATION_PREVIEW(hardwareType: string, computeTier: string, vCore: number, storage: number): string {
|
||||
return localize('sql.migration.sku.azureConfiguration.miPreview', "{0} - {1} - {2} vCore - {3} GB", hardwareType, computeTier, vCore, storage);
|
||||
}
|
||||
export const GENERAL_PURPOSE = localize('sql.migration.sku.azureConfiguration.generalPurpose', "General purpose");
|
||||
export const BUSINESS_CRITICAL = localize('sql.migration.sku.azureConfiguration.businessCritical', "Business critical");
|
||||
export const GEN5 = localize('sql.migration.sku.azureConfiguration.gen5', "Gen5");
|
||||
export const PREMIUM_SERIES = localize('sql.migration.sku.azureConfiguration.premiumSeries', "Premium-series");
|
||||
export const PREMIUM_SERIES_MEMORY_OPTIMIZED = localize('sql.migration.sku.azureConfiguration.premiumSeriesMemoryOptimized', "Memory optimized premium-series");
|
||||
|
||||
export const RECOMMENDATION_REASON = localize('sql.migration.sku.recommendationReason', "Recommendation reason");
|
||||
export const SOURCE_PROPERTIES = localize('sql.migration.sku.sourceProperties', "Source properties");
|
||||
|
||||
export const SQL_TEMPDB = localize('sql.migration.sku.sql.temp', "SQL tempdb");
|
||||
export const SQL_DATA_FILES = localize('sql.migration.sku.sql.dataDisk', "SQL data files");
|
||||
export const SQL_LOG_FILES = localize('sql.migration.sku.sql.logDisk', "SQL log files");
|
||||
export function STORAGE_CONFIGURATION(size: string, count: number): string {
|
||||
return localize('sql.migration.sku.azureConfiguration.storage', "{0} x {1}", size, count);
|
||||
}
|
||||
export const RECOMMENDED_TARGET_STORAGE_CONFIGURATION = localize('sql.migration.sku.targetStorageConfiguration', "Recommendation target storage configuration");
|
||||
export const RECOMMENDED_TARGET_STORAGE_CONFIGURATION_INFO = localize('sql.migration.sku.targetStorageConfiguration.info', "Below is the target storage configuration required to meet your storage performance needs.");
|
||||
export const STORAGE_HEADER = localize('sql.migration.sku.targetStorageConfiguration.storage', "Storage");
|
||||
export function STORAGE_GB(storage: number): string {
|
||||
return localize('sql.migration.sku.storageGB', "{0} GB", storage);
|
||||
}
|
||||
export const RECOMMENDED_STORAGE_CONFIGURATION = localize('sql.migration.sku.targetStorageConfiguration.recommendedStorageConfiguration', "Recommended storage configuration");
|
||||
export const EPHEMERAL_TEMPDB = localize('sql.migration.sku.targetStorageConfiguration.ephemeralTempdb', "Place tempdb on the local ephemeral SSD (default D:\\) drive");
|
||||
export const LOCAL_SSD = localize('sql.migration.sku.targetStorageConfiguration.local.SSD', "Local SSD");
|
||||
export const CACHING = localize('sql.migration.sku.targetStorageConfiguration.caching', "Host caching");
|
||||
export const CACHING_NA = localize('sql.migration.sku.targetStorageConfiguration.caching.na', "Not applicable");
|
||||
export const CACHING_NONE = localize('sql.migration.sku.targetStorageConfiguration.caching.none', "None");
|
||||
export const CACHING_READ_ONLY = localize('sql.migration.sku.targetStorageConfiguration.caching.readOnly', "Read-only");
|
||||
export const CACHING_READ_WRITE = localize('sql.migration.sku.targetStorageConfiguration.caching.readWrite', "Read/write");
|
||||
|
||||
export const DIMENSION = localize('sql.migration.sku.storage.dimension', "Dimension");
|
||||
export const VALUE = localize('sql.migration.sku.recommended.value', "Value");
|
||||
export const CPU_REQUIREMENT = localize('sql.migration.sku.cpu.requirement', "CPU requirement");
|
||||
export const MEMORY_REQUIREMENT = localize('sql.migration.sku.memory.requirement', "Memory requirement");
|
||||
export const DATA_STORAGE_REQUIREMENT = localize('sql.migration.sku.data.storage.requirement', "Data storage requirement");
|
||||
export const LOG_STORAGE_REQUIREMENT = localize('sql.migration.sku.log.storage.requirement', "Log storage requirement");
|
||||
export const DATA_IOPS_REQUIREMENT = localize('sql.migration.sku.data.iops.requirement', "Data IOPS requirement");
|
||||
export const LOGS_IOPS_REQUIREMENT = localize('sql.migration.sku.logs.iops.requirement', "Logs IOPS requirement");
|
||||
export const IO_LATENCY_REQUIREMENT = localize('sql.migration.sku.io.memory.requirement', "IO latency requirement");
|
||||
export function CPU_CORES(cpu: number): string {
|
||||
return localize('sql.migration.sku.cpu', "{0} cores", cpu.toFixed(2));
|
||||
}
|
||||
export function GB(gb: number): string {
|
||||
return localize('sql.migration.sku.gb', "{0} GB", gb.toFixed(2));
|
||||
}
|
||||
export function IOPS(iops: number): string {
|
||||
return localize('sql.migration.sku.iops', "{0} IOPS", iops.toFixed(2));
|
||||
}
|
||||
export function MS(ms: number): string {
|
||||
return localize('sql.migration.sku.ms', "{0} ms", ms.toFixed(2));
|
||||
}
|
||||
|
||||
export const RECOMMENDATION_PARAMETERS = localize('sql.migration.sku.parameters', "Recommendation parameters");
|
||||
export const EDIT_PARAMETERS = localize('sql.migration.sku.parameters.edit', "Edit parameters");
|
||||
export const EDIT_RECOMMENDATION_PARAMETERS = localize('sql.migration.sku.parameters.edit.title', "Edit recommendation parameters");
|
||||
export const EDIT_PARAMETERS_TEXT = localize('sql.migration.sku.parameters.text', "Enter the information below to edit the recommendation parameters.");
|
||||
export const UPDATE = localize('sql.migration.sku.parameters.update', "Update");
|
||||
export const ENABLE_PREVIEW_SKU = localize('sql.migration.sku.parameters.enable.preview', "Enable preview features");
|
||||
export const ENABLE_PREVIEW_SKU_INFO = localize('sql.migration.sku.parameters.enable.preview.info', "Enabling this option will include the latest hardware generations that have significantly improved performance and scalability. These SKUs are currently in Preview and may not yet be available in all regions.");
|
||||
export const SCALE_FACTOR = localize('sql.migration.sku.parameters.scale.factor', "Scale factor");
|
||||
export const SCALE_FACTOR_TOOLTIP = localize('sql.migration.sku.parameters.scale.factor.tooltip', "Change scale factor if you want the Azure recommendation to be a percentage larger or smaller than you current workload.");
|
||||
export const INVALID_SCALE_FACTOR = localize('sql.migration.sku.parameters.scale.factor.invalid', "Invalid scale factor. Enter a positive integer value.");
|
||||
export const PERCENTAGE_UTILIZATION = localize('sql.migration.sku.parameters.percentage.utilization', "Percentage utilization");
|
||||
export const PERCENTAGE_UTILIZATION_TOOLTIP = localize('sql.migration.sku.parameters.percentage.utilization.tooltip', "Percentile of data points to be used during aggregation of the performance data.");
|
||||
export function PERCENTAGE(val: number): string {
|
||||
return localize('sql.migration.sku.percentage', "{0}%", val);
|
||||
}
|
||||
export function PERCENTILE(val: string): string {
|
||||
return localize('sql.migration.sku.percentile', "{0}th percentile", val);
|
||||
}
|
||||
export const EMPTY_TIME = localize('sql.migration.sku.recommendations.empty.time', "-");
|
||||
export function LAST_REFRESHED_TIME(d: string = EMPTY_TIME): string {
|
||||
return localize('sql.migration.sku.recommendations.lastRefreshed', "Last refreshed: {0}", d);
|
||||
}
|
||||
|
||||
// Azure SQL Target
|
||||
export const AZURE_SQL_TARGET_PAGE_TITLE = localize('sql.migration.wizard.target.title', "Azure SQL target");
|
||||
export function AZURE_SQL_TARGET_PAGE_DESCRIPTION(targetInstance: string = 'instance'): string {
|
||||
return localize('sql.migration.wizard.target.description', "Select an Azure account and your target {0}.", targetInstance);
|
||||
}
|
||||
|
||||
// Managed Instance
|
||||
export const AZURE_SQL_DATABASE_MANAGED_INSTANCE = localize('sql.migration.azure.sql.database.managed.instance', "Azure SQL Managed Instance");
|
||||
export const NO_MANAGED_INSTANCE_FOUND = localize('sql.migration.no.managedInstance.found', "No managed instance found.");
|
||||
export const INVALID_MANAGED_INSTANCE_ERROR = localize('sql.migration.invalid.managedInstance.error', "To continue, select a valid managed instance.");
|
||||
|
||||
// Virtual Machine
|
||||
export const AZURE_SQL_DATABASE_VIRTUAL_MACHINE = localize('sql.migration.azure.sql.database.virtual.machine', "SQL Server on Azure Virtual Machines");
|
||||
export const AZURE_SQL_DATABASE_VIRTUAL_MACHINE_SHORT = localize('sql.migration.azure.sql.database.virtual.machine.short', "SQL Server on Azure VM");
|
||||
export const NO_VIRTUAL_MACHINE_FOUND = localize('sql.migration.no.virtualMachine.found', "No virtual machine found.");
|
||||
export const INVALID_VIRTUAL_MACHINE_ERROR = localize('sql.migration.invalid.virtualMachine.error', "To continue, select a valid virtual machine.");
|
||||
|
||||
// Azure SQL Database
|
||||
export const AZURE_SQL_DATABASE = localize('sql.migration.azure.sql.database', "Azure SQL Database");
|
||||
|
||||
// Target info tooltip
|
||||
export const TARGET_SUBSCRIPTION_INFO = localize('sql.migration.sku.subscription', "Subscription name for your Azure SQL target");
|
||||
export const TARGET_LOCATION_INFO = localize('sql.migration.sku.location', "Azure region for your Azure SQL target");
|
||||
export const TARGET_RESOURCE_GROUP_INFO = localize('sql.migration.sku.resource_group', "Resource group for your Azure SQL target");
|
||||
export const TARGET_RESOURCE_INFO = localize('sql.migration.sku.resource', "Your Azure SQL target resource name");
|
||||
|
||||
// Accounts page
|
||||
export const ACCOUNTS_SELECTION_PAGE_TITLE = localize('sql.migration.wizard.account.title', "Azure account");
|
||||
export const ACCOUNTS_SELECTION_PAGE_DESCRIPTION = localize('sql.migration.wizard.account.description', "Select an Azure account linked to Azure Data Studio, or link one now.");
|
||||
@@ -142,12 +319,14 @@ export const NETWORK_SHARE_PATH_FORMAT = localize('sql.migration.network.share.p
|
||||
export const WINDOWS_USER_ACCOUNT = localize('sql.migration.windows.user.account', "Domain\\username");
|
||||
export const NO_SUBSCRIPTIONS_FOUND = localize('sql.migration.no.subscription.found', "No subscription found.");
|
||||
export const NO_LOCATION_FOUND = localize('sql.migration.no.location.found', "No location found.");
|
||||
export const RESOURCE_GROUP_NOT_FOUND = localize('sql.migration.resource.group.not.found', "No resource groups found.");
|
||||
export const NO_STORAGE_ACCOUNT_FOUND = localize('sql.migration.no.storageAccount.found', "No storage account found.");
|
||||
export const NO_FILESHARES_FOUND = localize('sql.migration.no.fileShares.found', "No file shares found.");
|
||||
export const NO_BLOBCONTAINERS_FOUND = localize('sql.migration.no.blobContainers.found', "No blob containers found.");
|
||||
export const NO_BLOBFILES_FOUND = localize('sql.migration.no.blobFiles.found', "No blob files found.");
|
||||
export const INVALID_SUBSCRIPTION_ERROR = localize('sql.migration.invalid.subscription.error', "To continue, select a valid subscription.");
|
||||
export const INVALID_LOCATION_ERROR = localize('sql.migration.invalid.location.error', "To continue, select a valid location.");
|
||||
export const INVALID_RESOURCE_GROUP_ERROR = localize('sql.migration.invalid.resourceGroup.error', "To continue, select a valid resource group.");
|
||||
export const INVALID_STORAGE_ACCOUNT_ERROR = localize('sql.migration.invalid.storageAccount.error', "To continue, select a valid storage account.");
|
||||
export function INVALID_BLOB_RESOURCE_GROUP_ERROR(sourceDb: string): string {
|
||||
return localize('sql.migration.invalid.blob.resourceGroup.error', "To continue, select a valid resource group for source database '{0}'.", sourceDb);
|
||||
@@ -170,7 +349,7 @@ export function SQL_SOURCE_DETAILS(authMethod: MigrationSourceAuthenticationType
|
||||
case MigrationSourceAuthenticationType.Integrated:
|
||||
return localize('sql.migration.source.details.windowAuth', "Enter the Windows Authentication credentials used to connect to SQL Server instance {0}. These credentials will be used to connect to the SQL Server instance and identify valid backup files.", serverName);
|
||||
case MigrationSourceAuthenticationType.Sql:
|
||||
return localize('sql.migration.source.details.sqlAuth', "Enter the SQL Authentication credentials used to connect to SQL Server instance {0}. These credentials will be used to connect to the SQL Server instance and identify valid backup files.", serverName);
|
||||
return localize('sql.migration.source.details.sqlAuth', "Enter the SQL Authentication credentials used to connect to SQL Server instance {0}. These credentials will be used to connect to the SQL Server instance and identify valid backup files.", serverName);
|
||||
}
|
||||
}
|
||||
export const SELECT_RESOURCE_GROUP_PROMPT = localize('sql.migration.blob.resourceGroup.select.prompt', "Select a resource group value first.");
|
||||
@@ -189,7 +368,6 @@ export const AUTHENTICATION_KEYS = localize('sql.migration.authentication.types'
|
||||
export function SQL_MIGRATION_SERVICE_DETAILS_HEADER(sqlMigrationServiceName: string) {
|
||||
return localize('sql.migration.service.header', "Azure Database Migration Service \"{0}\" details:`", sqlMigrationServiceName);
|
||||
}
|
||||
export const DMS_PORTAL_INFO = localize('sql.migration.dms.portal.info', "Any existing Azure Database Migration Service in the Azure portal do not appear in Azure Data Studio. Any Database Migration Service created in Azure Data Studio will not be visible in the Azure portal yet.");
|
||||
export const DATABASE_MIGRATION_SERVICE_AUTHENTICATION_KEYS = localize('sql.migration.database.migration.service.authentication.keys', "Database Migration Service authentication keys");
|
||||
// create migration service dialog
|
||||
export const CREATE_MIGRATION_SERVICE_TITLE = localize('sql.migration.services.dialog.title', "Create Azure Database Migration Service");
|
||||
@@ -226,15 +404,9 @@ export function SERVICE_NOT_READY(serviceName: string): string {
|
||||
export function SERVICE_READY(serviceName: string, host: string): string {
|
||||
return localize('sql.migration.service.ready', "Azure Database Migration Service '{0}' is connected to self-hosted integration runtime running on the node - {1}", serviceName, host);
|
||||
}
|
||||
export const RESOURCE_GROUP_NOT_FOUND = localize('sql.migration.resource.group.not.found', "No resource groups found.");
|
||||
export const INVALID_RESOURCE_GROUP_ERROR = localize('sql.migration.invalid.resourceGroup.error', " To continue, select a valid resource group.");
|
||||
export const INVALID_SERVICE_NAME_ERROR = localize('sql.migration.invalid.service.name.error', "Enter a valid name for the Migration Service.");
|
||||
export const SERVICE_NOT_FOUND = localize('sql.migration.service.not.found', "No Migration Services found. To continue, create a new one.");
|
||||
export const SERVICE_STATUS_REFRESH_ERROR = localize('sql.migration.service.status.refresh.error', 'An error occurred while refreshing the migration service creation status.');
|
||||
export const MANAGED_INSTANCE = localize('sql.migration.managed.instance', "Azure SQL Managed Instance");
|
||||
export const NO_MANAGED_INSTANCE_FOUND = localize('sql.migration.no.managedInstance.found', "No managed instance found.");
|
||||
export const NO_VIRTUAL_MACHINE_FOUND = localize('sql.migration.no.virtualMachine.found', "No virtual machine found.");
|
||||
export const RESOURCE_GROUP_DESCRIPTION = localize('sql.migration.resource.group.description', "A resource group is a container that holds related resources for an Azure solution.");
|
||||
export const OK = localize('sql.migration.ok', "OK");
|
||||
export function NEW_RESOURCE_GROUP(resourceGroupName: string): string {
|
||||
return localize('sql.migration.new.resource.group', "(new) {0}", resourceGroupName);
|
||||
@@ -245,6 +417,7 @@ export const DMS_PROVISIONING_FAILED = localize('sql.migration.dms.provision.fai
|
||||
export const APPLY = localize('sql.migration.apply', "Apply");
|
||||
export const CREATING_RESOURCE_GROUP = localize('sql.migration.creating.rg.loading', "Creating resource group");
|
||||
export const RESOURCE_GROUP_CREATED = localize('sql.migration.rg.created', "Resource group created");
|
||||
export const RESOURCE_GROUP_DESCRIPTION = localize('sql.migration.resource.group.description', "A resource group is a container that holds related resources for an Azure solution.");
|
||||
export const NAME_OF_NEW_RESOURCE_GROUP = localize('sql.migration.name.of.new.rg', "Name of new resource group");
|
||||
export const DATA_UPLOADED_INFO = localize('sql.migration.data.uploaded.info', "Comparison of the actual amount of data read from the source and the actual amount of data uploaded to the target.");
|
||||
export const COPY_THROUGHPUT_INFO = localize('sql.migration.copy.throughput.info', "Data movement throughput achieved during the migration of your database backups to Azure. This is the rate of data transfer, calculated by data read divided by duration of backups migration to Azure.");
|
||||
@@ -355,8 +528,6 @@ export const BACKUP_START_TIME = localize('sql.migration.backup.start.time', "Ba
|
||||
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', "The cutover process cannot start until all the migrations are done. To return the latest file status, refresh your browser window.");
|
||||
export const AZURE_SQL_DATABASE_MANAGED_INSTANCE = localize('sql.migration.azure.sql.database.managed.instance', "Azure SQL Managed Instance");
|
||||
export const AZURE_SQL_DATABASE_VIRTUAL_MACHINE = localize('sql.migration.azure.sql.database.virtual.machine', "SQL Server on Azure Virtual Machines");
|
||||
export const CANCEL_MIGRATION = localize('sql.migration.cancel.migration', "Cancel migration");
|
||||
export function ACTIVE_BACKUP_FILES_ITEMS(fileCount: number) {
|
||||
if (fileCount === 1) {
|
||||
@@ -370,6 +541,7 @@ export const DETAILS_COPIED = localize('sql.migration.details.copied', "Details
|
||||
export const CANCEL_MIGRATION_CONFIRMATION = localize('sql.cancel.migration.confirmation', "Are you sure you want to cancel this migration?");
|
||||
export const YES = localize('sql.migration.yes', "Yes");
|
||||
export const NO = localize('sql.migration.no', "No");
|
||||
export const NA = localize('sql.migration.na', "N/A");
|
||||
export const EMPTY_TABLE_TEXT = localize('sql.migration.empty.table.text', "No backup files");
|
||||
export const EMPTY_TABLE_SUBTEXT = localize('sql.migration.empty.table.subtext', "If results were expected, verify the connection to the SQL Server instance.");
|
||||
export const MIGRATION_CUTOVER_ERROR = localize('sql.migration.cutover.error', 'An error occurred while initiating cutover.');
|
||||
@@ -379,7 +551,7 @@ export const COMPLETING_CUTOVER_WARNING = localize('sql.migration.completing.cut
|
||||
export const BUSINESS_CRITICAL_INFO = localize('sql.migration.bc.info', "A SQL Managed Instance migration cutover to the Business Critical service tier can take significantly longer than General Purpose because three secondary replicas have to be seeded for Always On High Availability group. The duration of the operation depends on the size of the data. Seeding speed in 90% of cases is 220 GB/hour or higher.");
|
||||
export const CUTOVER_HELP_MAIN = localize('sql.migration.cutover.help.main', "Perform the following steps before you complete cutover.");
|
||||
export const CUTOVER_HELP_STEP1 = localize('sql.migration.cutover.step.1', "1. Stop all incoming transactions to the source database.");
|
||||
export const CUTOVER_HELP_STEP2_NETWORK_SHARE = localize('sql.migration.cutover.step.2.network.share', "2. Create a final transaction log backup and store it on the network share.");
|
||||
export const CUTOVER_HELP_STEP2_NETWORK_SHARE = localize('sql.migration.cutover.step.2.network.share', "2. Create a final transaction log backup and store it on the network share.");
|
||||
export const CUTOVER_HELP_STEP2_BLOB_CONTAINER = localize('sql.migration.cutover.step.2.blob', "2. Create a final transaction log differential or backup and store it in the Azure Storage Blob Container.");
|
||||
export const CUTOVER_HELP_STEP3_NETWORK_SHARE = localize('sql.migration.cutover.step.3.network.share', "3. Verify that all log backups have been restored on the target database. The \"Log backups pending restore\" value should be zero.");
|
||||
export const CUTOVER_HELP_STEP3_BLOB_CONTAINER = localize('sql.migration.cutover.step.3.blob', "3. Verify that all backups have been restored on the target database. The \"Log backups pending restore\" value should be zero.");
|
||||
@@ -491,6 +663,7 @@ export const SQL_MIGRATION_SERVICE_DETAILS_STATUS_UNAVAILABLE = localize('sql.mi
|
||||
|
||||
//Source Credentials page.
|
||||
export const SAVE_AND_CLOSE = localize('sql.migration.save.close', "Save and close");
|
||||
export const SAVE_AND_CLOSE_POPUP = localize('sql.migration.save.close.popup', "Configuration saved. Performance data collection will remain running in the background. You can stop the collection when you want to.");
|
||||
export const SOURCE_CONFIGURATION = localize('sql.migration.source.configuration', "Source configuration");
|
||||
export const SOURCE_CREDENTIALS = localize('sql.migration.source.credentials', "Source credentials");
|
||||
export const ENTER_YOUR_SQL_CREDS = localize('sql.migration.enter.your.sql.cred', "Enter the credentials for the source SQL Server instance. These credentials will be used while migrating databases to Azure SQL.");
|
||||
@@ -498,7 +671,6 @@ export const SERVER = localize('sql.migration.server', "Server");
|
||||
export const USERNAME = localize('sql.migration.username', "Username");
|
||||
export const SIZE = localize('sql.migration.size', "Size (MB)");
|
||||
export const LAST_BACKUP = localize('sql.migration.last.backup', "Last backup");
|
||||
export const DATABASE_FOR_MIGRATION = localize('sql.migration.database.migration', "Databases for migration");
|
||||
export const DATABASE_MIGRATE_TEXT = localize('sql.migrate.text', "Select the databases that you want to migrate to Azure SQL.");
|
||||
export const OFFLINE_CAPS = localize('sql.migration.offline.caps', "OFFLINE");
|
||||
export const SELECT_DATABASE_TO_CONTINUE = localize('sql.migration.select.database.to.continue', "Please select 1 or more databases to assess for migration");
|
||||
@@ -534,7 +706,7 @@ export function DATABASES(selectedCount: number, totalCount: number): string {
|
||||
return localize('sql.migration.databases', "Databases ({0}/{1})", selectedCount, totalCount);
|
||||
}
|
||||
export function DATABASES_SELECTED(selectedCount: number, totalCount: number): string {
|
||||
return localize('sql.migration.databases.selected', "{0}/{1} Databases selected", selectedCount, totalCount);
|
||||
return localize('sql.migration.databases.selected', "{0}/{1} databases selected", selectedCount, totalCount);
|
||||
}
|
||||
export function ISSUES_COUNT(totalCount: number): string {
|
||||
return localize('sql.migration.issues.count', "Issues ({0})", totalCount);
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as vscode from 'vscode';
|
||||
import * as mssql from '../../../../mssql';
|
||||
import { azureResource } from 'azureResource';
|
||||
import { getLocations, getResourceGroupFromId, getBlobContainerId, getFullResourceGroupFromId, getResourceName } from '../../api/azure';
|
||||
import { MigrationMode, MigrationStateModel, NetworkContainerType, SavedInfo, Page } from '../../models/stateMachine';
|
||||
import { MigrationMode, MigrationStateModel, NetworkContainerType, SavedInfo } from '../../models/stateMachine';
|
||||
import { MigrationContext } from '../../models/migrationLocalStorage';
|
||||
import { WizardController } from '../../wizard/wizardController';
|
||||
import { getMigrationModeEnum, getMigrationTargetTypeEnum } from '../../constants/helper';
|
||||
@@ -28,7 +28,7 @@ export class RetryMigrationDialog {
|
||||
const sourceDatabaseName = migration.migrationContext.properties.sourceDatabaseName;
|
||||
let savedInfo: SavedInfo;
|
||||
savedInfo = {
|
||||
closedPage: Page.AzureAccount,
|
||||
closedPage: 0,
|
||||
|
||||
// AzureAccount
|
||||
azureAccount: migration.azureAccount,
|
||||
@@ -42,6 +42,7 @@ export class RetryMigrationDialog {
|
||||
databaseList: [sourceDatabaseName],
|
||||
migrationDatabases: [],
|
||||
serverAssessment: null,
|
||||
skuRecommendation: null,
|
||||
|
||||
migrationTargetType: getMigrationTargetTypeEnum(migration)!,
|
||||
subscription: migration.subscription,
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { MigrationStateModel, PerformanceDataSourceOptions } from '../../models/stateMachine';
|
||||
import * as constants from '../../constants/strings';
|
||||
import * as styles from '../../constants/styles';
|
||||
import * as utils from '../../api/utils';
|
||||
import { SKURecommendationPage } from '../../wizard/skuRecommendationPage';
|
||||
import { EOL } from 'os';
|
||||
|
||||
export class GetAzureRecommendationDialog {
|
||||
private static readonly StartButtonText: string = constants.AZURE_RECOMMENDATION_START;
|
||||
|
||||
private dialog: azdata.window.Dialog | undefined;
|
||||
private _isOpen: boolean = false;
|
||||
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
private _performanceDataSource!: PerformanceDataSourceOptions;
|
||||
|
||||
private _collectDataContainer!: azdata.FlexContainer;
|
||||
private _collectDataFolderInput!: azdata.InputBoxComponent;
|
||||
|
||||
private _openExistingContainer!: azdata.FlexContainer;
|
||||
private _openExistingFolderInput!: azdata.InputBoxComponent;
|
||||
|
||||
constructor(public skuRecommendationPage: SKURecommendationPage, public wizard: azdata.window.Wizard, public migrationStateModel: MigrationStateModel) {
|
||||
this._performanceDataSource = PerformanceDataSourceOptions.CollectData;
|
||||
}
|
||||
|
||||
private async initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
dialog.registerContent(async (view) => {
|
||||
try {
|
||||
const flex = this.createContainer(view);
|
||||
|
||||
this._disposables.push(view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
|
||||
await view.initializeModel(flex);
|
||||
resolve();
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private createContainer(_view: azdata.ModelView): azdata.FlexContainer {
|
||||
const container = _view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'margin': '8px 16px',
|
||||
'flex-direction': 'column',
|
||||
}
|
||||
}).component();
|
||||
const description1 = _view.modelBuilder.text().withProps({
|
||||
value: constants.AZURE_RECOMMENDATION_DESCRIPTION,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component();
|
||||
const description2 = _view.modelBuilder.text().withProps({
|
||||
value: constants.AZURE_RECOMMENDATION_DESCRIPTION2,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin-top': '8px',
|
||||
}
|
||||
}).component();
|
||||
const selectDataSourceRadioButtons = this.createDataSourceContainer(_view);
|
||||
container.addItems([
|
||||
description1,
|
||||
description2,
|
||||
selectDataSourceRadioButtons,
|
||||
]);
|
||||
return container;
|
||||
}
|
||||
|
||||
private createDataSourceContainer(_view: azdata.ModelView): azdata.FlexContainer {
|
||||
const chooseMethodText = _view.modelBuilder.text().withProps({
|
||||
value: constants.AZURE_RECOMMENDATION_CHOOSE_METHOD,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin-top': '16px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const buttonGroup = 'dataSourceContainer';
|
||||
const radioButtonContainer = _view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'row',
|
||||
'width': 'fit-content',
|
||||
'margin': '4px 0 16px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const collectDataButton = _view.modelBuilder.radioButton()
|
||||
.withProps({
|
||||
name: buttonGroup,
|
||||
label: constants.AZURE_RECOMMENDATION_COLLECT_DATA,
|
||||
checked: this._performanceDataSource === PerformanceDataSourceOptions.CollectData,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0'
|
||||
},
|
||||
}).component();
|
||||
this._disposables.push(collectDataButton.onDidChangeCheckedState(async (e) => {
|
||||
if (e) {
|
||||
await this.switchDataSourceContainerFields(PerformanceDataSourceOptions.CollectData);
|
||||
}
|
||||
}));
|
||||
|
||||
const openExistingButton = _view.modelBuilder.radioButton()
|
||||
.withProps({
|
||||
name: buttonGroup,
|
||||
label: constants.AZURE_RECOMMENDATION_OPEN_EXISTING,
|
||||
checked: this._performanceDataSource === PerformanceDataSourceOptions.OpenExisting,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0 12px',
|
||||
}
|
||||
}).component();
|
||||
this._disposables.push(openExistingButton.onDidChangeCheckedState(async (e) => {
|
||||
if (e) {
|
||||
await this.switchDataSourceContainerFields(PerformanceDataSourceOptions.OpenExisting);
|
||||
}
|
||||
}));
|
||||
|
||||
radioButtonContainer.addItems([
|
||||
collectDataButton,
|
||||
openExistingButton
|
||||
]);
|
||||
|
||||
this._collectDataContainer = this.createCollectDataContainer(_view);
|
||||
this._openExistingContainer = this.createOpenExistingContainer(_view);
|
||||
|
||||
const container = _view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).withItems([
|
||||
chooseMethodText,
|
||||
radioButtonContainer,
|
||||
this._openExistingContainer,
|
||||
this._collectDataContainer,
|
||||
]).component();
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private createCollectDataContainer(_view: azdata.ModelView): azdata.FlexContainer {
|
||||
const container = _view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'column',
|
||||
'display': 'inline',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const instructions = _view.modelBuilder.text().withProps({
|
||||
value: constants.AZURE_RECOMMENDATION_COLLECT_DATA_FOLDER,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin-bottom': '8px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const selectFolderContainer = _view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'row',
|
||||
'align-items': 'center',
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._collectDataFolderInput = _view.modelBuilder.inputBox().withProps({
|
||||
placeHolder: constants.FOLDER_NAME,
|
||||
readOnly: true,
|
||||
width: 320,
|
||||
CSSStyles: {
|
||||
'margin-right': '12px'
|
||||
},
|
||||
}).component();
|
||||
this._disposables.push(this._collectDataFolderInput.onTextChanged(async (value) => {
|
||||
if (value) {
|
||||
this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim();
|
||||
this.dialog!.okButton.enabled = true;
|
||||
}
|
||||
}));
|
||||
|
||||
const browseButton = _view.modelBuilder.button().withProps({
|
||||
label: constants.BROWSE,
|
||||
width: 100,
|
||||
CSSStyles: {
|
||||
'margin': '0'
|
||||
}
|
||||
}).component();
|
||||
this._disposables.push(browseButton.onDidClick(async (e) => {
|
||||
let folder = await this.handleBrowse();
|
||||
this._collectDataFolderInput.value = folder;
|
||||
}));
|
||||
|
||||
selectFolderContainer.addItems([
|
||||
this._collectDataFolderInput,
|
||||
browseButton,
|
||||
]);
|
||||
|
||||
container.addItems([
|
||||
instructions,
|
||||
selectFolderContainer,
|
||||
]);
|
||||
return container;
|
||||
}
|
||||
|
||||
private createOpenExistingContainer(_view: azdata.ModelView): azdata.FlexContainer {
|
||||
const container = _view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'column',
|
||||
'display': 'none',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const instructions = _view.modelBuilder.text().withProps({
|
||||
value: constants.AZURE_RECOMMENDATION_OPEN_EXISTING_FOLDER,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin-bottom': '8px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const selectFolderContainer = _view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'row',
|
||||
'align-items': 'center',
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._openExistingFolderInput = _view.modelBuilder.inputBox().withProps({
|
||||
placeHolder: constants.FOLDER_NAME,
|
||||
readOnly: true,
|
||||
width: 320,
|
||||
CSSStyles: {
|
||||
'margin-right': '12px'
|
||||
},
|
||||
}).component();
|
||||
this._disposables.push(this._openExistingFolderInput.onTextChanged(async (value) => {
|
||||
if (value) {
|
||||
this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim();
|
||||
this.dialog!.okButton.enabled = true;
|
||||
}
|
||||
}));
|
||||
|
||||
const openButton = _view.modelBuilder.button().withProps({
|
||||
label: constants.OPEN,
|
||||
width: 100,
|
||||
CSSStyles: {
|
||||
'margin': '0'
|
||||
}
|
||||
}).component();
|
||||
this._disposables.push(openButton.onDidClick(async (e) => {
|
||||
let folder = await this.handleBrowse();
|
||||
this._openExistingFolderInput.value = folder;
|
||||
}));
|
||||
|
||||
selectFolderContainer.addItems([
|
||||
this._openExistingFolderInput,
|
||||
openButton,
|
||||
]);
|
||||
container.addItems([
|
||||
instructions,
|
||||
selectFolderContainer,
|
||||
]);
|
||||
return container;
|
||||
}
|
||||
|
||||
private async switchDataSourceContainerFields(containerType: PerformanceDataSourceOptions): Promise<void> {
|
||||
this._performanceDataSource = containerType;
|
||||
|
||||
let okButtonEnabled = false;
|
||||
switch (containerType) {
|
||||
case PerformanceDataSourceOptions.CollectData: {
|
||||
await this._collectDataContainer.updateCssStyles({ 'display': 'inline' });
|
||||
await this._openExistingContainer.updateCssStyles({ 'display': 'none' });
|
||||
|
||||
if (this._collectDataFolderInput.value) {
|
||||
okButtonEnabled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PerformanceDataSourceOptions.OpenExisting: {
|
||||
await this._collectDataContainer.updateCssStyles({ 'display': 'none' });
|
||||
await this._openExistingContainer.updateCssStyles({ 'display': 'inline' });
|
||||
|
||||
if (this._openExistingFolderInput.value) {
|
||||
okButtonEnabled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.dialog!.okButton.enabled = okButtonEnabled;
|
||||
}
|
||||
|
||||
public async openDialog(dialogName?: string) {
|
||||
if (!this._isOpen) {
|
||||
this._isOpen = true;
|
||||
this.dialog = azdata.window.createModelViewDialog(constants.GET_AZURE_RECOMMENDATION, 'GetAzureRecommendationsDialog', 'narrow');
|
||||
|
||||
this.dialog.okButton.label = GetAzureRecommendationDialog.StartButtonText;
|
||||
this._disposables.push(this.dialog.okButton.onClick(async () => await this.execute()));
|
||||
this.dialog.cancelButton.onClick(() => this._isOpen = false);
|
||||
|
||||
const dialogSetupPromises: Thenable<void>[] = [];
|
||||
dialogSetupPromises.push(this.initializeDialog(this.dialog));
|
||||
azdata.window.openDialog(this.dialog);
|
||||
await Promise.all(dialogSetupPromises);
|
||||
|
||||
// if data source was previously selected, default folder value to previously selected
|
||||
switch (this.migrationStateModel._skuRecommendationPerformanceDataSource) {
|
||||
case PerformanceDataSourceOptions.CollectData: {
|
||||
this._collectDataFolderInput.value = this.migrationStateModel._skuRecommendationPerformanceLocation;
|
||||
break;
|
||||
}
|
||||
case PerformanceDataSourceOptions.OpenExisting: {
|
||||
this._openExistingFolderInput.value = this.migrationStateModel._skuRecommendationPerformanceLocation;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await this.switchDataSourceContainerFields(this._performanceDataSource);
|
||||
}
|
||||
}
|
||||
|
||||
protected async execute() {
|
||||
this._isOpen = false;
|
||||
|
||||
this.migrationStateModel._skuRecommendationPerformanceDataSource = this._performanceDataSource;
|
||||
switch (this.migrationStateModel._skuRecommendationPerformanceDataSource) {
|
||||
case PerformanceDataSourceOptions.CollectData: {
|
||||
await this.migrationStateModel.startPerfDataCollection(
|
||||
this.migrationStateModel._skuRecommendationPerformanceLocation,
|
||||
this.migrationStateModel._performanceDataQueryIntervalInSeconds,
|
||||
this.migrationStateModel._staticDataQueryIntervalInSeconds,
|
||||
this.migrationStateModel._numberOfPerformanceDataQueryIterations,
|
||||
this.skuRecommendationPage
|
||||
);
|
||||
break;
|
||||
}
|
||||
case PerformanceDataSourceOptions.OpenExisting: {
|
||||
const serverName = (await this.migrationStateModel.getSourceConnectionProfile()).serverName;
|
||||
const errors: string[] = [];
|
||||
try {
|
||||
void vscode.window.showInformationMessage(constants.AZURE_RECOMMENDATION_OPEN_EXISTING_POPUP);
|
||||
|
||||
await this.skuRecommendationPage.startCardLoading();
|
||||
await this.migrationStateModel.getSkuRecommendations();
|
||||
|
||||
const skuRecommendationError = this.migrationStateModel._skuRecommendationResults?.recommendationError;
|
||||
if (skuRecommendationError) {
|
||||
errors.push(`message: ${skuRecommendationError.message}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
errors.push(constants.SKU_RECOMMENDATION_ASSESSMENT_UNEXPECTED_ERROR(serverName, e));
|
||||
} finally {
|
||||
if (errors.length > 0) {
|
||||
this.wizard.message = {
|
||||
text: constants.SKU_RECOMMENDATION_ERROR(serverName),
|
||||
description: errors.join(EOL),
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.skuRecommendationPage.refreshSkuRecommendationComponents();
|
||||
}
|
||||
|
||||
public get isOpen(): boolean {
|
||||
return this._isOpen;
|
||||
}
|
||||
|
||||
// TO-DO: add validation
|
||||
private async handleBrowse(): Promise<string> {
|
||||
let path = '';
|
||||
|
||||
let options: vscode.OpenDialogOptions = {
|
||||
defaultUri: vscode.Uri.file(utils.getUserHome()!),
|
||||
canSelectFiles: false,
|
||||
canSelectFolders: true,
|
||||
canSelectMany: false,
|
||||
};
|
||||
|
||||
let fileUris = await vscode.window.showOpenDialog(options);
|
||||
if (fileUris && fileUris?.length > 0 && fileUris[0]) {
|
||||
path = fileUris[0].fsPath;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { MigrationStateModel } from '../../models/stateMachine';
|
||||
import * as constants from '../../constants/strings';
|
||||
import * as styles from '../../constants/styles';
|
||||
import { selectDropDownIndex } from '../../api/utils';
|
||||
import { SKURecommendationPage } from '../../wizard/skuRecommendationPage';
|
||||
|
||||
export const TARGET_PERCENTILE_VALUES = [99, 97, 95, 90, 75, 50];
|
||||
|
||||
export class SkuEditParametersDialog {
|
||||
private static readonly UpdateButtonText: string = constants.UPDATE;
|
||||
|
||||
private dialog: azdata.window.Dialog | undefined;
|
||||
private _isOpen: boolean = false;
|
||||
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
private _scaleFactorInput!: azdata.InputBoxComponent;
|
||||
private _targetPercentileDropdown!: azdata.DropDownComponent;
|
||||
private _enablePreviewValue!: boolean;
|
||||
|
||||
constructor(public skuRecommendationPage: SKURecommendationPage, public migrationStateModel: MigrationStateModel) {
|
||||
this._enablePreviewValue = true;
|
||||
}
|
||||
|
||||
private async initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
dialog.registerContent(async (view) => {
|
||||
try {
|
||||
const flex = this.createContainer(view);
|
||||
|
||||
this._disposables.push(view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
|
||||
await view.initializeModel(flex);
|
||||
resolve();
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private createContainer(_view: azdata.ModelView): azdata.FlexContainer {
|
||||
const container = _view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'margin': '8px 16px',
|
||||
'flex-direction': 'column',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const description = _view.modelBuilder.text().withProps({
|
||||
value: constants.EDIT_PARAMETERS_TEXT,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component();
|
||||
|
||||
const WIZARD_INPUT_COMPONENT_WIDTH = '300px';
|
||||
const scaleFactorLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.SCALE_FACTOR,
|
||||
description: constants.SCALE_FACTOR_TOOLTIP,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS
|
||||
}
|
||||
}).component();
|
||||
this._scaleFactorInput = _view.modelBuilder.inputBox().withProps({
|
||||
required: true,
|
||||
validationErrorMessage: constants.INVALID_SCALE_FACTOR,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
CSSStyles: {
|
||||
'margin-top': '-1em',
|
||||
'margin-bottom': '8px',
|
||||
},
|
||||
}).withValidation(c => {
|
||||
if (Number(c.value) && Number(c.value) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}).component();
|
||||
|
||||
const targetPercentileLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.PERCENTAGE_UTILIZATION,
|
||||
description: constants.PERCENTAGE_UTILIZATION_TOOLTIP,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
}
|
||||
}).component();
|
||||
const createPercentageValues = () => {
|
||||
let values: azdata.CategoryValue[] = [];
|
||||
TARGET_PERCENTILE_VALUES.forEach(n => {
|
||||
const val = n.toString();
|
||||
values.push({
|
||||
displayName: constants.PERCENTILE(val),
|
||||
name: val,
|
||||
});
|
||||
});
|
||||
return values;
|
||||
};
|
||||
this._targetPercentileDropdown = _view.modelBuilder.dropDown().withProps({
|
||||
values: createPercentageValues(),
|
||||
ariaLabel: constants.PERCENTAGE_UTILIZATION,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: false,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
CSSStyles: {
|
||||
'margin-top': '-1em',
|
||||
'margin-bottom': '8px',
|
||||
},
|
||||
}).component();
|
||||
|
||||
const enablePreviewLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.ENABLE_PREVIEW_SKU,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
}
|
||||
}).component();
|
||||
const buttonGroup = 'enablePreviewSKUs';
|
||||
const enablePreviewRadioButtonContainer = _view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'row',
|
||||
'width': 'fit-content',
|
||||
'margin-top': '-1em',
|
||||
'margin-bottom': '8px',
|
||||
}
|
||||
}).component();
|
||||
const enablePreviewButton = _view.modelBuilder.radioButton()
|
||||
.withProps({
|
||||
name: buttonGroup,
|
||||
label: constants.YES,
|
||||
checked: this._enablePreviewValue,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'width': 'fit-content',
|
||||
'margin': '0'
|
||||
},
|
||||
}).component();
|
||||
this._disposables.push(enablePreviewButton.onDidChangeCheckedState(async (e) => {
|
||||
if (e) {
|
||||
this._enablePreviewValue = true;
|
||||
}
|
||||
}));
|
||||
const disablePreviewButton = _view.modelBuilder.radioButton()
|
||||
.withProps({
|
||||
name: buttonGroup,
|
||||
label: constants.NO,
|
||||
checked: !this._enablePreviewValue,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'width': 'fit-content',
|
||||
'margin': '0 12px',
|
||||
}
|
||||
}).component();
|
||||
this._disposables.push(disablePreviewButton.onDidChangeCheckedState(async (e) => {
|
||||
if (e) {
|
||||
this._enablePreviewValue = false;
|
||||
}
|
||||
}));
|
||||
enablePreviewRadioButtonContainer.addItems([
|
||||
enablePreviewButton,
|
||||
disablePreviewButton
|
||||
]);
|
||||
|
||||
const enablePreviewInfoBox = _view.modelBuilder.infoBox()
|
||||
.withProps({
|
||||
text: constants.ENABLE_PREVIEW_SKU_INFO,
|
||||
style: 'information',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component();
|
||||
|
||||
container.addItems([
|
||||
description,
|
||||
scaleFactorLabel,
|
||||
this._scaleFactorInput,
|
||||
targetPercentileLabel,
|
||||
this._targetPercentileDropdown,
|
||||
enablePreviewLabel,
|
||||
enablePreviewRadioButtonContainer,
|
||||
enablePreviewInfoBox,
|
||||
]);
|
||||
return container;
|
||||
}
|
||||
|
||||
public async openDialog(dialogName?: string) {
|
||||
if (!this._isOpen) {
|
||||
this._isOpen = true;
|
||||
this.dialog = azdata.window.createModelViewDialog(constants.EDIT_RECOMMENDATION_PARAMETERS, 'SkuEditParametersDialog', 'narrow');
|
||||
|
||||
this.dialog.okButton.label = SkuEditParametersDialog.UpdateButtonText;
|
||||
this._disposables.push(this.dialog.okButton.onClick(async () => await this.execute()));
|
||||
|
||||
this.dialog.cancelButton.onClick(() => this._isOpen = false);
|
||||
|
||||
const dialogSetupPromises: Thenable<void>[] = [];
|
||||
dialogSetupPromises.push(this.initializeDialog(this.dialog));
|
||||
azdata.window.openDialog(this.dialog);
|
||||
await Promise.all(dialogSetupPromises);
|
||||
|
||||
this._scaleFactorInput.value = this.migrationStateModel._skuScalingFactor.toString();
|
||||
this._enablePreviewValue = this.migrationStateModel._skuEnablePreview;
|
||||
(<azdata.CategoryValue[]>this._targetPercentileDropdown.values)?.forEach((percentile, index) => {
|
||||
if ((<azdata.CategoryValue>percentile).name.toLowerCase() === this.migrationStateModel._skuTargetPercentile.toString()) {
|
||||
selectDropDownIndex(this._targetPercentileDropdown, index);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected async execute() {
|
||||
this._isOpen = false;
|
||||
this.migrationStateModel._skuScalingFactor = Number(this._scaleFactorInput.value!);
|
||||
this.migrationStateModel._skuTargetPercentile = Number((<azdata.CategoryValue>this._targetPercentileDropdown.value).name);
|
||||
this.migrationStateModel._skuEnablePreview = this._enablePreviewValue;
|
||||
await this.skuRecommendationPage.refreshSkuParameters();
|
||||
}
|
||||
|
||||
public get isOpen(): boolean {
|
||||
return this._isOpen;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { MigrationStateModel, MigrationTargetType } from '../../models/stateMachine';
|
||||
import * as constants from '../../constants/strings';
|
||||
import * as styles from '../../constants/styles';
|
||||
import * as mssql from '../../../../mssql';
|
||||
|
||||
export class SkuRecommendationResultsDialog {
|
||||
|
||||
private static readonly OpenButtonText: string = 'Close';
|
||||
// private static readonly CreateTargetButtonText: string = 'Create target in portal';
|
||||
|
||||
private _isOpen: boolean = false;
|
||||
private dialog: azdata.window.Dialog | undefined;
|
||||
|
||||
// Dialog Name for Telemetry
|
||||
public dialogName: string | undefined;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
public title?: string;
|
||||
public targetName?: string;
|
||||
|
||||
public targetRecommendations?: mssql.SkuRecommendationResultItem[];
|
||||
public instanceRequirements?: mssql.SqlInstanceRequirements;
|
||||
|
||||
constructor(public model: MigrationStateModel, public _targetType: MigrationTargetType) {
|
||||
switch (this._targetType) {
|
||||
case MigrationTargetType.SQLMI:
|
||||
this.targetName = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLVM:
|
||||
this.targetName = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLDB:
|
||||
this.targetName = constants.AZURE_SQL_DATABASE;
|
||||
break;
|
||||
}
|
||||
|
||||
this.title = constants.RECOMMENDATIONS_TITLE(this.targetName);
|
||||
}
|
||||
|
||||
private async initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
dialog.registerContent(async (view) => {
|
||||
try {
|
||||
const flex = this.createContainer(view);
|
||||
|
||||
this._disposables.push(view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
|
||||
await view.initializeModel(flex);
|
||||
resolve();
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
private createContainer(_view: azdata.ModelView): azdata.FlexContainer {
|
||||
const container = _view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'margin': '8px 16px',
|
||||
'flex-direction': 'column',
|
||||
}
|
||||
}).component();
|
||||
|
||||
this.targetRecommendations?.forEach((recommendation, index) => {
|
||||
if (index > 0) {
|
||||
const separator = _view.modelBuilder.separator().withProps({ width: 750 }).component();
|
||||
container.addItem(separator);
|
||||
}
|
||||
|
||||
container.addItem(this.createRecommendation(_view, recommendation));
|
||||
});
|
||||
return container;
|
||||
}
|
||||
|
||||
private createRecommendation(_view: azdata.ModelView, recommendationItem: mssql.SkuRecommendationResultItem): azdata.FlexContainer {
|
||||
|
||||
let recommendation: mssql.IaaSSkuRecommendationResultItem | mssql.PaaSSkuRecommendationResultItem;
|
||||
|
||||
let configuration = constants.NA;
|
||||
let storageSection = _view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
switch (this._targetType) {
|
||||
case MigrationTargetType.SQLVM:
|
||||
recommendation = <mssql.IaaSSkuRecommendationResultItem>recommendationItem;
|
||||
|
||||
if (recommendation.targetSku) {
|
||||
configuration = constants.VM_CONFIGURATION(recommendation.targetSku.virtualMachineSize!.azureSkuName, recommendation.targetSku.virtualMachineSize!.vCPUsAvailable);
|
||||
|
||||
storageSection = this.createSqlVmTargetStorageSection(_view, recommendation);
|
||||
}
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLMI:
|
||||
case MigrationTargetType.SQLDB:
|
||||
recommendation = <mssql.PaaSSkuRecommendationResultItem>recommendationItem;
|
||||
|
||||
if (recommendation.targetSku) {
|
||||
const serviceTier = recommendation.targetSku.category?.sqlServiceTier === mssql.AzureSqlPaaSServiceTier.GeneralPurpose
|
||||
? constants.GENERAL_PURPOSE
|
||||
: constants.BUSINESS_CRITICAL;
|
||||
|
||||
const hardwareType = recommendation.targetSku.category?.hardwareType === mssql.AzureSqlPaaSHardwareType.Gen5
|
||||
? constants.GEN5
|
||||
: recommendation.targetSku.category?.hardwareType === mssql.AzureSqlPaaSHardwareType.PremiumSeries
|
||||
? constants.PREMIUM_SERIES
|
||||
: constants.PREMIUM_SERIES_MEMORY_OPTIMIZED;
|
||||
|
||||
configuration = this._targetType === MigrationTargetType.SQLDB
|
||||
? constants.DB_CONFIGURATION(serviceTier, recommendation.targetSku.computeSize!)
|
||||
: constants.MI_CONFIGURATION(hardwareType, serviceTier, recommendation.targetSku.computeSize!);
|
||||
|
||||
const storageLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.STORAGE_HEADER,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin': '12px 0 0',
|
||||
}
|
||||
}).component();
|
||||
const storageValue = _view.modelBuilder.text().withProps({
|
||||
value: constants.STORAGE_GB(recommendation.targetSku.storageMaxSizeInMb! / 1024),
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component();
|
||||
|
||||
storageSection.addItems([
|
||||
storageLabel,
|
||||
storageValue,
|
||||
]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
const recommendationContainer = _view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'margin-bottom': '20px',
|
||||
'flex-direction': 'column',
|
||||
}
|
||||
}).component();
|
||||
|
||||
if (this._targetType === MigrationTargetType.SQLDB) {
|
||||
const databaseNameLabel = _view.modelBuilder.text().withProps({
|
||||
value: recommendation.databaseName!,
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
}
|
||||
}).component();
|
||||
recommendationContainer.addItem(databaseNameLabel);
|
||||
}
|
||||
|
||||
const targetDeploymentTypeLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.TARGET_DEPLOYMENT_TYPE,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin': '0',
|
||||
}
|
||||
}).component();
|
||||
const targetDeploymentTypeValue = _view.modelBuilder.text().withProps({
|
||||
value: this.targetName,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const azureConfigurationLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.AZURE_CONFIGURATION,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin': '12px 0 0',
|
||||
}
|
||||
}).component();
|
||||
const azureConfigurationValue = _view.modelBuilder.text().withProps({
|
||||
value: configuration,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0',
|
||||
}
|
||||
}).component();
|
||||
|
||||
recommendationContainer.addItems([
|
||||
targetDeploymentTypeLabel,
|
||||
targetDeploymentTypeValue,
|
||||
|
||||
targetDeploymentTypeLabel,
|
||||
targetDeploymentTypeValue,
|
||||
|
||||
azureConfigurationLabel,
|
||||
azureConfigurationValue,
|
||||
|
||||
storageSection
|
||||
]);
|
||||
|
||||
const recommendationsReasonSection = _view.modelBuilder.text().withProps({
|
||||
value: constants.RECOMMENDATION_REASON,
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
'margin': '12px 0 0'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const reasonsContainer = _view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
const justifications: string[] = recommendation?.positiveJustifications?.concat(recommendation?.negativeJustifications) || [constants.SKU_RECOMMENDATION_NO_RECOMMENDATION_REASON];
|
||||
justifications?.forEach(text => {
|
||||
reasonsContainer.addItem(
|
||||
_view.modelBuilder.text().withProps({
|
||||
value: text,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component()
|
||||
);
|
||||
});
|
||||
|
||||
const storagePropertiesContainer = this.createStoragePropertiesTable(_view, recommendation?.databaseName);
|
||||
|
||||
recommendationContainer.addItems([
|
||||
recommendationsReasonSection,
|
||||
reasonsContainer,
|
||||
storagePropertiesContainer,
|
||||
]);
|
||||
|
||||
return recommendationContainer;
|
||||
}
|
||||
|
||||
private createSqlVmTargetStorageSection(_view: azdata.ModelView, recommendation: mssql.IaaSSkuRecommendationResultItem): azdata.FlexContainer {
|
||||
const recommendedTargetStorageSection = _view.modelBuilder.text().withProps({
|
||||
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
'margin-top': '12px'
|
||||
}
|
||||
}).component();
|
||||
const recommendedTargetStorageInfo = _view.modelBuilder.text().withProps({
|
||||
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION_INFO,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component();
|
||||
|
||||
const headerCssStyle = {
|
||||
'border': 'none',
|
||||
'text-align': 'left',
|
||||
'white-space': 'nowrap',
|
||||
'text-overflow': 'ellipsis',
|
||||
'overflow': 'hidden',
|
||||
'border-bottom': '1px solid'
|
||||
};
|
||||
|
||||
const rowCssStyle = {
|
||||
'border': 'none',
|
||||
'text-align': 'left',
|
||||
'white-space': 'nowrap',
|
||||
'text-overflow': 'ellipsis',
|
||||
'overflow': 'hidden',
|
||||
};
|
||||
|
||||
const columnWidth = 150;
|
||||
let columns: azdata.DeclarativeTableColumn[] = [
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.STORAGE_HEADER,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
},
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.RECOMMENDED_STORAGE_CONFIGURATION,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
},
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.CACHING,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
}
|
||||
];
|
||||
|
||||
const tempTableRow: azdata.DeclarativeTableCellValue[] = [
|
||||
{ value: constants.SQL_TEMPDB },
|
||||
{
|
||||
value: recommendation.targetSku.tempDbDiskSizes!.length > 0
|
||||
? constants.STORAGE_CONFIGURATION(recommendation.targetSku.logDiskSizes![0].size, recommendation.targetSku.logDiskSizes!.length)
|
||||
: constants.EPHEMERAL_TEMPDB
|
||||
},
|
||||
{
|
||||
value: recommendation.targetSku.tempDbDiskSizes!.length > 0
|
||||
? this.getCachingText(recommendation.targetSku.logDiskSizes![0].caching)
|
||||
: constants.CACHING_NA
|
||||
}
|
||||
];
|
||||
|
||||
const dataDiskTableRow: azdata.DeclarativeTableCellValue[] = [
|
||||
{ value: constants.SQL_DATA_FILES },
|
||||
{ value: constants.STORAGE_CONFIGURATION(recommendation.targetSku.dataDiskSizes![0].size, recommendation.targetSku.dataDiskSizes!.length) },
|
||||
{ value: this.getCachingText(recommendation.targetSku.dataDiskSizes![0].caching) }
|
||||
];
|
||||
|
||||
const logDiskTableRow: azdata.DeclarativeTableCellValue[] = [
|
||||
{ value: constants.SQL_LOG_FILES },
|
||||
{ value: constants.STORAGE_CONFIGURATION(recommendation.targetSku.logDiskSizes![0].size, recommendation.targetSku.logDiskSizes!.length) },
|
||||
{ value: this.getCachingText(recommendation.targetSku.logDiskSizes![0].caching) }
|
||||
];
|
||||
|
||||
let storageConfigurationTableRows: azdata.DeclarativeTableCellValue[][] = [
|
||||
tempTableRow,
|
||||
dataDiskTableRow,
|
||||
logDiskTableRow,
|
||||
];
|
||||
|
||||
const storageConfigurationTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable().withProps({
|
||||
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
|
||||
columns: columns,
|
||||
dataValues: storageConfigurationTableRows,
|
||||
width: 700
|
||||
}).component();
|
||||
|
||||
const container = _view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).withItems([
|
||||
recommendedTargetStorageSection,
|
||||
recommendedTargetStorageInfo,
|
||||
storageConfigurationTable,
|
||||
]).component();
|
||||
return container;
|
||||
}
|
||||
|
||||
private getCachingText(caching: mssql.AzureManagedDiskCaching): string {
|
||||
switch (caching) {
|
||||
case mssql.AzureManagedDiskCaching.NotApplicable:
|
||||
return constants.CACHING_NA;
|
||||
|
||||
case mssql.AzureManagedDiskCaching.None:
|
||||
return constants.CACHING_NONE;
|
||||
|
||||
case mssql.AzureManagedDiskCaching.ReadOnly:
|
||||
return constants.CACHING_READ_ONLY;
|
||||
|
||||
case mssql.AzureManagedDiskCaching.ReadWrite:
|
||||
return constants.CACHING_READ_WRITE;
|
||||
}
|
||||
}
|
||||
|
||||
private createStoragePropertiesTable(_view: azdata.ModelView, databaseName?: string): azdata.FlexContainer {
|
||||
let instanceRequirements;
|
||||
switch (this._targetType) {
|
||||
case MigrationTargetType.SQLVM:
|
||||
case MigrationTargetType.SQLMI:
|
||||
instanceRequirements = this.instanceRequirements;
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLDB:
|
||||
instanceRequirements = this.instanceRequirements?.databaseLevelRequirements.filter(d => {
|
||||
return databaseName === d.databaseName;
|
||||
})[0]!;
|
||||
break;
|
||||
}
|
||||
|
||||
const storagePropertiesSection = _view.modelBuilder.text().withProps({
|
||||
value: constants.SOURCE_PROPERTIES,
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
'margin-top': '12px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const headerCssStyle = {
|
||||
'border': 'none',
|
||||
'text-align': 'left',
|
||||
'white-space': 'nowrap',
|
||||
'text-overflow': 'ellipsis',
|
||||
'overflow': 'hidden',
|
||||
'border-bottom': '1px solid'
|
||||
};
|
||||
|
||||
const rowCssStyle = {
|
||||
'border': 'none',
|
||||
'text-align': 'left',
|
||||
'white-space': 'nowrap',
|
||||
'text-overflow': 'ellipsis',
|
||||
'overflow': 'hidden',
|
||||
};
|
||||
|
||||
const columnWidth = 80;
|
||||
let columns: azdata.DeclarativeTableColumn[] = [
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.DIMENSION,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
},
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.VALUE,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
}
|
||||
];
|
||||
|
||||
const createRow = (dimension: string, value: string) => {
|
||||
const row: azdata.DeclarativeTableCellValue[] = [
|
||||
{ value: dimension },
|
||||
{ value: value }
|
||||
];
|
||||
return row;
|
||||
};
|
||||
const cpuRow = createRow(constants.CPU_REQUIREMENT, constants.CPU_CORES(instanceRequirements?.cpuRequirementInCores!));
|
||||
const memoryRow = createRow(constants.MEMORY_REQUIREMENT, constants.GB(instanceRequirements?.memoryRequirementInMB! / 1024));
|
||||
const dataStorageRow = createRow(constants.DATA_STORAGE_REQUIREMENT, constants.GB(instanceRequirements?.dataStorageRequirementInMB! / 1024));
|
||||
const logStorageRow = createRow(constants.LOG_STORAGE_REQUIREMENT, constants.GB(instanceRequirements?.logStorageRequirementInMB! / 1024));
|
||||
const dataIOPSRow = createRow(constants.DATA_IOPS_REQUIREMENT, constants.IOPS(instanceRequirements?.dataIOPSRequirement!));
|
||||
const logsIOPSRow = createRow(constants.LOGS_IOPS_REQUIREMENT, constants.IOPS(instanceRequirements?.logIOPSRequirement!));
|
||||
const ioLatencyRow = createRow(constants.IO_LATENCY_REQUIREMENT, instanceRequirements?.ioThroughputRequirementInMBps! < 5 ? constants.NA : constants.MS(instanceRequirements?.ioLatencyRequirementInMs!));
|
||||
const storagePropertiesTableRows: azdata.DeclarativeTableCellValue[][] = [
|
||||
cpuRow,
|
||||
memoryRow,
|
||||
dataStorageRow,
|
||||
logStorageRow,
|
||||
dataIOPSRow,
|
||||
logsIOPSRow,
|
||||
ioLatencyRow,
|
||||
];
|
||||
|
||||
const storagePropertiesTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable().withProps({
|
||||
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
|
||||
columns: columns,
|
||||
dataValues: storagePropertiesTableRows,
|
||||
width: 300
|
||||
}).component();
|
||||
|
||||
const container = _view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).withItems([
|
||||
storagePropertiesSection,
|
||||
storagePropertiesTable,
|
||||
]).component();
|
||||
return container;
|
||||
}
|
||||
|
||||
public async openDialog(dialogName?: string, recommendations?: mssql.SkuRecommendationResult) {
|
||||
if (!this._isOpen) {
|
||||
this._isOpen = true;
|
||||
this.instanceRequirements = recommendations?.instanceRequirements;
|
||||
|
||||
switch (this._targetType) {
|
||||
case MigrationTargetType.SQLMI:
|
||||
this.targetRecommendations = recommendations?.sqlMiRecommendationResults;
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLVM:
|
||||
this.targetRecommendations = recommendations?.sqlVmRecommendationResults;
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLDB:
|
||||
this.targetRecommendations = recommendations?.sqlDbRecommendationResults;
|
||||
break;
|
||||
}
|
||||
|
||||
this.dialog = azdata.window.createModelViewDialog(this.title!, 'SkuRecommendationResultsDialog', 'medium');
|
||||
|
||||
this.dialog.okButton.label = SkuRecommendationResultsDialog.OpenButtonText;
|
||||
this._disposables.push(this.dialog.okButton.onClick(async () => await this.execute()));
|
||||
|
||||
this.dialog.cancelButton.hidden = true;
|
||||
// TO-DO: When "Create target in Portal" feature is ready, unhide cancel button and use cancelButton to direct user to Portal
|
||||
// this.dialog.cancelButton.label = SkuRecommendationResultsDialog.CreateTargetButtonText;
|
||||
// this._disposables.push(this.dialog.cancelButton.onClick(async () => console.log(SkuRecommendationResultsDialog.CreateTargetButtonText)));
|
||||
|
||||
const dialogSetupPromises: Thenable<void>[] = [];
|
||||
dialogSetupPromises.push(this.initializeDialog(this.dialog));
|
||||
azdata.window.openDialog(this.dialog);
|
||||
await Promise.all(dialogSetupPromises);
|
||||
}
|
||||
}
|
||||
|
||||
protected async execute() {
|
||||
this._isOpen = false;
|
||||
}
|
||||
|
||||
public get isOpen(): boolean {
|
||||
return this._isOpen;
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,13 @@ import * as azurecore from 'azurecore';
|
||||
import * as vscode from 'vscode';
|
||||
import * as mssql from '../../../mssql';
|
||||
import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getSqlMigrationServices, getSubscriptions, SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer, getLocations, getResourceGroups, getLocationDisplayName, getSqlManagedInstanceDatabases, getBlobs } from '../api/azure';
|
||||
import { SKURecommendations } from './externalContract';
|
||||
import * as constants from '../constants/strings';
|
||||
import { MigrationLocalStorage } from './migrationLocalStorage';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../telemtery';
|
||||
import { hashString, deepClone } from '../api/utils';
|
||||
import { SKURecommendationPage } from '../wizard/skuRecommendationPage';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export enum State {
|
||||
@@ -59,9 +59,9 @@ export enum NetworkContainerType {
|
||||
}
|
||||
|
||||
export enum Page {
|
||||
AzureAccount,
|
||||
DatabaseSelector,
|
||||
SKURecommendation,
|
||||
TargetSelection,
|
||||
MigrationMode,
|
||||
DatabaseBackup,
|
||||
IntegrationRuntime,
|
||||
@@ -74,6 +74,10 @@ export enum WizardEntryPoint {
|
||||
RetryMigration = 'RetryMigration',
|
||||
}
|
||||
|
||||
export enum PerformanceDataSourceOptions {
|
||||
CollectData = 'CollectData',
|
||||
OpenExisting = 'OpenExisting',
|
||||
}
|
||||
export interface DatabaseBackupModel {
|
||||
migrationMode: MigrationMode;
|
||||
networkContainerType: NetworkContainerType;
|
||||
@@ -103,7 +107,6 @@ export interface Model {
|
||||
readonly sourceConnectionId: string;
|
||||
readonly currentState: State;
|
||||
gatheringInformationError: string | undefined;
|
||||
skuRecommendations: SKURecommendations | undefined;
|
||||
_azureAccount: azdata.Account | undefined;
|
||||
_databaseBackup: DatabaseBackupModel | undefined;
|
||||
}
|
||||
@@ -134,8 +137,18 @@ export interface SavedInfo {
|
||||
blobs: Blob[];
|
||||
targetDatabaseNames: string[];
|
||||
migrationServiceId: string | null;
|
||||
skuRecommendation: SkuRecommendationSavedInfo | null;
|
||||
}
|
||||
|
||||
export interface SkuRecommendationSavedInfo {
|
||||
skuRecommendationPerformanceDataSource: PerformanceDataSourceOptions;
|
||||
skuRecommendationPerformanceLocation: string;
|
||||
perfDataCollectionStartDate?: Date;
|
||||
perfDataCollectionStopDate?: Date;
|
||||
skuTargetPercentile: number;
|
||||
skuScalingFactor: number;
|
||||
skuEnablePreview: boolean;
|
||||
}
|
||||
|
||||
export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
public _azureAccounts!: azdata.Account[];
|
||||
@@ -176,12 +189,42 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
private _currentState: State;
|
||||
private _gatheringInformationError: string | undefined;
|
||||
|
||||
private _skuRecommendations: SKURecommendations | undefined;
|
||||
public _assessmentResults!: ServerAssessment;
|
||||
public _runAssessments: boolean = true;
|
||||
private _assessmentApiResponse!: mssql.AssessmentResult;
|
||||
public mementoString: string;
|
||||
|
||||
public _skuRecommendationResults!: SkuRecommendation;
|
||||
public _skuRecommendationPerformanceDataSource!: PerformanceDataSourceOptions;
|
||||
private _skuRecommendationApiResponse!: mssql.SkuRecommendationResult;
|
||||
public _skuRecommendationPerformanceLocation!: string;
|
||||
private _skuRecommendationRecommendedDatabaseList!: string[];
|
||||
private _startPerfDataCollectionApiResponse!: mssql.StartPerfDataCollectionResult;
|
||||
private _stopPerfDataCollectionApiResponse!: mssql.StopPerfDataCollectionResult;
|
||||
private _refreshPerfDataCollectionApiResponse!: mssql.RefreshPerfDataCollectionResult;
|
||||
public _perfDataCollectionStartDate!: Date | undefined;
|
||||
public _perfDataCollectionStopDate!: Date | undefined;
|
||||
public _perfDataCollectionLastRefreshedDate!: Date;
|
||||
public _perfDataCollectionMessages!: string[];
|
||||
public _perfDataCollectionErrors!: string[];
|
||||
public _perfDataCollectionIsCollecting!: boolean;
|
||||
|
||||
public readonly _performanceDataQueryIntervalInSeconds = 30;
|
||||
public readonly _staticDataQueryIntervalInSeconds = 60;
|
||||
public readonly _numberOfPerformanceDataQueryIterations = 19;
|
||||
public readonly _defaultDataPointStartTime = '1900-01-01 00:00:00';
|
||||
public readonly _defaultDataPointEndTime = '2200-01-01 00:00:00';
|
||||
public readonly _recommendationTargetPlatforms = [MigrationTargetType.SQLDB, MigrationTargetType.SQLMI, MigrationTargetType.SQLVM];
|
||||
|
||||
public refreshPerfDataCollectionFrequency = this._performanceDataQueryIntervalInSeconds * 1000;
|
||||
private _autoRefreshPerfDataCollectionHandle!: NodeJS.Timeout;
|
||||
public refreshGetSkuRecommendationFrequency = 600000; // 10 minutes
|
||||
private _autoRefreshGetSkuRecommendationHandle!: NodeJS.Timeout;
|
||||
|
||||
public _skuScalingFactor!: number;
|
||||
public _skuTargetPercentile!: number;
|
||||
public _skuEnablePreview!: boolean;
|
||||
|
||||
public _vmDbs: string[] = [];
|
||||
public _miDbs: string[] = [];
|
||||
public _targetType!: MigrationTargetType;
|
||||
@@ -213,6 +256,10 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
this._databaseBackup.networkShares = [];
|
||||
this._databaseBackup.blobs = [];
|
||||
this.mementoString = 'sqlMigration.assessmentResults';
|
||||
|
||||
this._skuScalingFactor = 100;
|
||||
this._skuTargetPercentile = 95;
|
||||
this._skuEnablePreview = true;
|
||||
}
|
||||
|
||||
public get sourceConnectionId(): string {
|
||||
@@ -233,6 +280,17 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
let finalResult = temp.filter((name) => !this.excludeDbs.includes(name));
|
||||
return finalResult;
|
||||
}
|
||||
public hasRecommendedDatabaseListChanged(): boolean {
|
||||
const oldDbList = this._skuRecommendationRecommendedDatabaseList;
|
||||
const newDbList = this._databaseAssessment;
|
||||
|
||||
if (!oldDbList || !newDbList) {
|
||||
return false;
|
||||
}
|
||||
return !((oldDbList.length === newDbList.length) && oldDbList.every(function (element, index) {
|
||||
return element === newDbList[index];
|
||||
}));
|
||||
}
|
||||
|
||||
public async getDatabaseAssessments(targetType: MigrationTargetType): Promise<ServerAssessment> {
|
||||
const ownerUri = await azdata.connection.getUriForConnection(this.sourceConnectionId);
|
||||
@@ -291,6 +349,314 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
return this._assessmentResults;
|
||||
}
|
||||
|
||||
public async getSkuRecommendations(): Promise<SkuRecommendation> {
|
||||
try {
|
||||
const serverInfo = await azdata.connection.getServerInfo(this.sourceConnectionId);
|
||||
const machineName = (<any>serverInfo)['machineName']; // get actual machine name instead of whatever the user entered as the server name (e.g. DESKTOP-xxx instead of localhost)
|
||||
|
||||
const response = (await this.migrationService.getSkuRecommendations(
|
||||
this._skuRecommendationPerformanceLocation,
|
||||
this._performanceDataQueryIntervalInSeconds,
|
||||
this._recommendationTargetPlatforms.map(p => p.toString()),
|
||||
machineName,
|
||||
this._skuTargetPercentile,
|
||||
this._skuScalingFactor,
|
||||
this._defaultDataPointStartTime,
|
||||
this._defaultDataPointEndTime,
|
||||
this._skuEnablePreview,
|
||||
this._databaseAssessment))!;
|
||||
this._skuRecommendationApiResponse = response;
|
||||
|
||||
// clone list of databases currently being assessed and store them, so that if the user ever changes the list we can refresh new recommendations
|
||||
this._skuRecommendationRecommendedDatabaseList = this._databaseAssessment.slice();
|
||||
|
||||
console.log('sqlinstancerequirements: ');
|
||||
console.log(this._skuRecommendationApiResponse.instanceRequirements);
|
||||
|
||||
if (response?.sqlDbRecommendationResults || response?.sqlMiRecommendationResults || response?.sqlVmRecommendationResults) {
|
||||
this._skuRecommendationResults = {
|
||||
recommendations: {
|
||||
sqlDbRecommendationResults: response?.sqlDbRecommendationResults ?? [],
|
||||
sqlMiRecommendationResults: response?.sqlMiRecommendationResults ?? [],
|
||||
sqlVmRecommendationResults: response?.sqlVmRecommendationResults ?? [],
|
||||
instanceRequirements: response?.instanceRequirements
|
||||
},
|
||||
};
|
||||
} else {
|
||||
this._skuRecommendationResults = {
|
||||
recommendations: {
|
||||
sqlDbRecommendationResults: [],
|
||||
sqlMiRecommendationResults: [],
|
||||
sqlVmRecommendationResults: [],
|
||||
instanceRequirements: response?.instanceRequirements
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
this._skuRecommendationResults = {
|
||||
recommendations: {
|
||||
sqlDbRecommendationResults: this._skuRecommendationApiResponse?.sqlDbRecommendationResults ?? [],
|
||||
sqlMiRecommendationResults: this._skuRecommendationApiResponse?.sqlMiRecommendationResults ?? [],
|
||||
sqlVmRecommendationResults: this._skuRecommendationApiResponse?.sqlVmRecommendationResults ?? [],
|
||||
instanceRequirements: this._skuRecommendationApiResponse?.instanceRequirements
|
||||
},
|
||||
recommendationError: error
|
||||
};
|
||||
} // Generating all the telemetry asynchronously as we don't need to block the user for it.
|
||||
this.generateSkuRecommendationTelemetry().catch(e => console.error(e));
|
||||
|
||||
return this._skuRecommendationResults;
|
||||
}
|
||||
|
||||
private async generateSkuRecommendationTelemetry(): Promise<void> {
|
||||
try {
|
||||
|
||||
this._skuRecommendationResults?.recommendations?.sqlMiRecommendationResults?.forEach(resultItem => {
|
||||
// Send telemetry for recommended MI SKU
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.SkuRecommendationWizard,
|
||||
TelemetryAction.GetMISkuRecommendation,
|
||||
{
|
||||
'sessionId': this._sessionId,
|
||||
'recommendedSku': JSON.stringify(resultItem?.targetSku)
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
this._skuRecommendationResults?.recommendations?.sqlVmRecommendationResults?.forEach(resultItem => {
|
||||
// Send telemetry for recommended VM SKU
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.SkuRecommendationWizard,
|
||||
TelemetryAction.GetVMSkuRecommendation,
|
||||
{
|
||||
'sessionId': this._sessionId,
|
||||
'recommendedSku': JSON.stringify(resultItem?.targetSku)
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
// Send Instance requirements used for calculating recommendations
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.SkuRecommendationWizard,
|
||||
TelemetryAction.GetInstanceRequirements,
|
||||
{
|
||||
'sessionId': this._sessionId,
|
||||
'performanceDataSource': this._skuRecommendationPerformanceDataSource,
|
||||
'databaseLevelRequirements': JSON.stringify(this._skuRecommendationResults?.recommendations?.instanceRequirements?.databaseLevelRequirements?.map(i => {
|
||||
return {
|
||||
cpuRequirementInCores: i.cpuRequirementInCores,
|
||||
dataIOPSRequirement: i.dataIOPSRequirement,
|
||||
logIOPSRequirement: i.logIOPSRequirement,
|
||||
ioLatencyRequirementInMs: i.ioLatencyRequirementInMs,
|
||||
ioThroughputRequirementInMBps: i.ioThroughputRequirementInMBps,
|
||||
dataStorageRequirementInMB: i.dataStorageRequirementInMB,
|
||||
logStorageRequirementInMB: i.logStorageRequirementInMB,
|
||||
databaseName: hashString(i.databaseName),
|
||||
memoryRequirementInMB: i.memoryRequirementInMB,
|
||||
cpuRequirementInPercentageOfTotalInstance: i.cpuRequirementInPercentageOfTotalInstance,
|
||||
numberOfDataPointsAnalyzed: i.numberOfDataPointsAnalyzed,
|
||||
fileLevelRequirements: i.fileLevelRequirements?.map(file => {
|
||||
return {
|
||||
fileType: file.fileType,
|
||||
sizeInMB: file.sizeInMB,
|
||||
readLatencyInMs: file.readLatencyInMs,
|
||||
writeLatencyInMs: file.writeLatencyInMs,
|
||||
iopsRequirement: file.iopsRequirement,
|
||||
ioThroughputRequirementInMBps: file.ioThroughputRequirementInMBps,
|
||||
numberOfDataPointsAnalyzed: file.numberOfDataPointsAnalyzed
|
||||
};
|
||||
})
|
||||
};
|
||||
}))
|
||||
},
|
||||
{
|
||||
'cpuRequirementInCores': this._skuRecommendationResults?.recommendations?.instanceRequirements?.cpuRequirementInCores,
|
||||
'dataStorageRequirementInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.dataStorageRequirementInMB,
|
||||
'logStorageRequirementInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.logStorageRequirementInMB,
|
||||
'memoryRequirementInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.memoryRequirementInMB,
|
||||
'dataIOPSRequirement': this._skuRecommendationResults?.recommendations?.instanceRequirements?.dataIOPSRequirement,
|
||||
'logIOPSRequirement': this._skuRecommendationResults?.recommendations?.instanceRequirements?.logIOPSRequirement,
|
||||
'ioLatencyRequirementInMs': this._skuRecommendationResults?.recommendations?.instanceRequirements?.ioLatencyRequirementInMs,
|
||||
'ioThroughputRequirementInMBps': this._skuRecommendationResults?.recommendations?.instanceRequirements?.ioThroughputRequirementInMBps,
|
||||
'tempDBSizeInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.tempDBSizeInMB,
|
||||
'aggregationTargetPercentile': this._skuRecommendationResults?.recommendations?.instanceRequirements?.aggregationTargetPercentile,
|
||||
'numberOfDataPointsAnalyzed': this._skuRecommendationResults?.recommendations?.instanceRequirements?.numberOfDataPointsAnalyzed,
|
||||
}
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async startPerfDataCollection(
|
||||
dataFolder: string,
|
||||
perfQueryIntervalInSec: number,
|
||||
staticQueryIntervalInSec: number,
|
||||
numberOfIterations: number,
|
||||
page: SKURecommendationPage): Promise<boolean> {
|
||||
try {
|
||||
if (!this.performanceCollectionInProgress()) {
|
||||
const ownerUri = await azdata.connection.getUriForConnection(this.sourceConnectionId);
|
||||
const response = await this.migrationService.startPerfDataCollection(ownerUri, dataFolder, perfQueryIntervalInSec, staticQueryIntervalInSec, numberOfIterations);
|
||||
|
||||
this._startPerfDataCollectionApiResponse = response!;
|
||||
this._perfDataCollectionStartDate = this._startPerfDataCollectionApiResponse.dateTimeStarted;
|
||||
this._perfDataCollectionStopDate = undefined;
|
||||
|
||||
void vscode.window.showInformationMessage(constants.AZURE_RECOMMENDATION_START_POPUP);
|
||||
|
||||
await this.startSkuTimers(page, this.refreshPerfDataCollectionFrequency);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
// Generate telemetry for start data collection request
|
||||
this.generateStartDataCollectionTelemetry().catch(e => console.error(e));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async generateStartDataCollectionTelemetry(): Promise<void> {
|
||||
try {
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.DataCollectionWizard,
|
||||
TelemetryAction.StartDataCollection,
|
||||
{
|
||||
'sessionId': this._sessionId,
|
||||
'timeDataCollectionStarted': this._perfDataCollectionStartDate?.toString() || ''
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async startSkuTimers(page: SKURecommendationPage, refreshIntervalInMs: number): Promise<void> {
|
||||
const classVariable = this;
|
||||
|
||||
if (!this._autoRefreshPerfDataCollectionHandle) {
|
||||
clearInterval(this._autoRefreshPerfDataCollectionHandle);
|
||||
if (this.refreshPerfDataCollectionFrequency !== -1) {
|
||||
this._autoRefreshPerfDataCollectionHandle = setInterval(async function () {
|
||||
await classVariable.refreshPerfDataCollection();
|
||||
|
||||
if (await classVariable.isWaitingForFirstTimeRefresh()) {
|
||||
await page.refreshSkuRecommendationComponents(); // update timer
|
||||
}
|
||||
}, refreshIntervalInMs);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._autoRefreshGetSkuRecommendationHandle) {
|
||||
// start one-time timer to get SKU recommendation
|
||||
clearTimeout(this._autoRefreshGetSkuRecommendationHandle);
|
||||
if (this.refreshGetSkuRecommendationFrequency !== -1) {
|
||||
this._autoRefreshGetSkuRecommendationHandle = setTimeout(async function () {
|
||||
await page.refreshAzureRecommendation();
|
||||
}, this.refreshGetSkuRecommendationFrequency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async stopPerfDataCollection(): Promise<boolean> {
|
||||
try {
|
||||
const response = await this.migrationService.stopPerfDataCollection();
|
||||
void vscode.window.showInformationMessage(constants.AZURE_RECOMMENDATION_STOP_POPUP);
|
||||
|
||||
this._stopPerfDataCollectionApiResponse = response!;
|
||||
this._perfDataCollectionStopDate = this._stopPerfDataCollectionApiResponse.dateTimeStopped;
|
||||
|
||||
// stop auto refresh
|
||||
clearInterval(this._autoRefreshPerfDataCollectionHandle);
|
||||
clearInterval(this._autoRefreshGetSkuRecommendationHandle);
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
// Generate telemetry for stop data collection request
|
||||
this.generateStopDataCollectionTelemetry().catch(e => console.error(e));
|
||||
return true;
|
||||
}
|
||||
|
||||
private async generateStopDataCollectionTelemetry(): Promise<void> {
|
||||
try {
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.DataCollectionWizard,
|
||||
TelemetryAction.StopDataCollection,
|
||||
{
|
||||
'sessionId': this._sessionId,
|
||||
'timeDataCollectionStopped': this._perfDataCollectionStopDate?.toString() || ''
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async refreshPerfDataCollection(): Promise<boolean> {
|
||||
try {
|
||||
const response = await this.migrationService.refreshPerfDataCollection(this._perfDataCollectionLastRefreshedDate ?? new Date());
|
||||
this._refreshPerfDataCollectionApiResponse = response!;
|
||||
this._perfDataCollectionLastRefreshedDate = this._refreshPerfDataCollectionApiResponse.refreshTime;
|
||||
this._perfDataCollectionMessages = this._refreshPerfDataCollectionApiResponse.messages;
|
||||
this._perfDataCollectionErrors = this._refreshPerfDataCollectionApiResponse.errors;
|
||||
this._perfDataCollectionIsCollecting = this._refreshPerfDataCollectionApiResponse.isCollecting;
|
||||
|
||||
if (this._perfDataCollectionErrors?.length > 0) {
|
||||
void vscode.window.showInformationMessage(constants.PERF_DATA_COLLECTION_ERROR(this._assessmentApiResponse?.assessmentResult?.name, this._perfDataCollectionErrors));
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async isWaitingForFirstTimeRefresh(): Promise<boolean> {
|
||||
const elapsedTimeInMins = Math.abs(new Date().getTime() - new Date(this._perfDataCollectionStartDate!).getTime()) / 60000;
|
||||
const skuRecAutoRefreshTimeInMins = this.refreshGetSkuRecommendationFrequency / 60000;
|
||||
|
||||
return elapsedTimeInMins < skuRecAutoRefreshTimeInMins;
|
||||
}
|
||||
|
||||
public performanceCollectionNotStarted(): boolean {
|
||||
if (!this._perfDataCollectionStartDate
|
||||
&& !this._perfDataCollectionStopDate) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public performanceCollectionInProgress(): boolean {
|
||||
if (this._perfDataCollectionStartDate
|
||||
&& !this._perfDataCollectionStopDate) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public performanceCollectionStopped(): boolean {
|
||||
if (this._perfDataCollectionStartDate
|
||||
&& this._perfDataCollectionStopDate) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async generateAssessmentTelemetry(): Promise<void> {
|
||||
try {
|
||||
|
||||
@@ -324,9 +690,6 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
TelemetryAction.ServerAssessment,
|
||||
{
|
||||
'sessionId': this._sessionId,
|
||||
'tenantId': this._azureAccount.properties.tenants[0].id,
|
||||
'subscriptionId': this._targetSubscription?.id,
|
||||
'resourceGroup': this._resourceGroup?.name,
|
||||
'hashedServerName': hashString(this._assessmentApiResponse?.assessmentResult?.name),
|
||||
'startTime': startTime.toString(),
|
||||
'endTime': endTime.toString(),
|
||||
@@ -360,8 +723,6 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
TelemetryAction.DatabaseAssessment,
|
||||
{
|
||||
'sessionId': this._sessionId,
|
||||
'subscriptionId': this._targetSubscription?.id,
|
||||
'resourceGroup': this._resourceGroup?.name,
|
||||
'hashedDatabaseName': hashString(d.name),
|
||||
'compatibilityLevel': d.compatibilityLevel
|
||||
},
|
||||
@@ -398,8 +759,6 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
TelemetryAction.DatabaseAssessmentWarning,
|
||||
{
|
||||
'sessionId': this._sessionId,
|
||||
'subscriptionId': this._targetSubscription?.id,
|
||||
'resourceGroup': this._resourceGroup?.name,
|
||||
'warnings': JSON.stringify(databaseWarnings)
|
||||
},
|
||||
{}
|
||||
@@ -418,14 +777,13 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
TelemetryAction.DatabaseAssessmentError,
|
||||
{
|
||||
'sessionId': this._sessionId,
|
||||
'subscriptionId': this._targetSubscription?.id,
|
||||
'resourceGroup': this._resourceGroup?.name,
|
||||
'errors': JSON.stringify(databaseErrors)
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
console.log('error during assessment telemetry:');
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
@@ -438,14 +796,6 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
this._gatheringInformationError = error;
|
||||
}
|
||||
|
||||
public get skuRecommendations(): SKURecommendations | undefined {
|
||||
return this._skuRecommendations;
|
||||
}
|
||||
|
||||
public set skuRecommendations(recommendations: SKURecommendations | undefined) {
|
||||
this._skuRecommendations = recommendations;
|
||||
}
|
||||
|
||||
public get stateChangeEvent(): vscode.Event<StateChangeEvent> {
|
||||
return this._stateChangeEventEmitter.event;
|
||||
}
|
||||
@@ -1068,6 +1418,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
blobs: [],
|
||||
targetDatabaseNames: [],
|
||||
migrationServiceId: null,
|
||||
skuRecommendation: null,
|
||||
};
|
||||
switch (currentPage) {
|
||||
case Page.Summary:
|
||||
@@ -1081,23 +1432,40 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
saveInfo.targetSubscription = this._databaseBackup.subscription;
|
||||
saveInfo.blobs = this._databaseBackup.blobs;
|
||||
saveInfo.targetDatabaseNames = this._targetDatabaseNames;
|
||||
|
||||
case Page.MigrationMode:
|
||||
saveInfo.migrationMode = this._databaseBackup.migrationMode;
|
||||
|
||||
case Page.TargetSelection:
|
||||
saveInfo.azureAccount = deepClone(this._azureAccount);
|
||||
saveInfo.azureTenant = deepClone(this._azureTenant);
|
||||
saveInfo.subscription = this._targetSubscription;
|
||||
saveInfo.location = this._location;
|
||||
saveInfo.resourceGroup = this._resourceGroup;
|
||||
saveInfo.targetServerInstance = this._targetServerInstance;
|
||||
|
||||
case Page.SKURecommendation:
|
||||
saveInfo.migrationTargetType = this._targetType;
|
||||
saveInfo.databaseAssessment = this._databaseAssessment;
|
||||
saveInfo.serverAssessment = this._assessmentResults;
|
||||
saveInfo.migrationDatabases = this._databaseSelection;
|
||||
saveInfo.databaseList = this._migrationDbs;
|
||||
saveInfo.subscription = this._targetSubscription;
|
||||
saveInfo.location = this._location;
|
||||
saveInfo.resourceGroup = this._resourceGroup;
|
||||
saveInfo.targetServerInstance = this._targetServerInstance;
|
||||
|
||||
if (this._skuRecommendationPerformanceDataSource) {
|
||||
let skuRecommendation: SkuRecommendationSavedInfo = {
|
||||
skuRecommendationPerformanceDataSource: this._skuRecommendationPerformanceDataSource,
|
||||
skuRecommendationPerformanceLocation: this._skuRecommendationPerformanceLocation,
|
||||
perfDataCollectionStartDate: this._perfDataCollectionStartDate,
|
||||
perfDataCollectionStopDate: this._perfDataCollectionStopDate,
|
||||
skuTargetPercentile: this._skuTargetPercentile,
|
||||
skuScalingFactor: this._skuScalingFactor,
|
||||
skuEnablePreview: this._skuEnablePreview,
|
||||
};
|
||||
saveInfo.skuRecommendation = skuRecommendation;
|
||||
}
|
||||
|
||||
case Page.DatabaseSelector:
|
||||
saveInfo.selectedDatabases = this.databaseSelectorTableValues;
|
||||
case Page.AzureAccount:
|
||||
saveInfo.azureAccount = deepClone(this._azureAccount);
|
||||
saveInfo.azureTenant = deepClone(this._azureTenant);
|
||||
await this.extensionContext.globalState.update(`${this.mementoString}.${serverName}`, saveInfo);
|
||||
}
|
||||
}
|
||||
@@ -1113,3 +1481,8 @@ export interface ServerAssessment {
|
||||
errors?: mssql.ErrorModel[];
|
||||
assessmentError?: Error;
|
||||
}
|
||||
|
||||
export interface SkuRecommendation {
|
||||
recommendations: mssql.SkuRecommendationResult;
|
||||
recommendationError?: Error;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ export enum TelemetryViews {
|
||||
MigrationWizardController = 'MigrationWizardController',
|
||||
StartMigrationService = 'StartMigrationSerivce',
|
||||
SqlMigrationWizard = 'SqlMigrationWizard',
|
||||
MigrationLocalStorage = 'MigrationLocalStorage'
|
||||
MigrationLocalStorage = 'MigrationLocalStorage',
|
||||
SkuRecommendationWizard = 'SkuRecommendationWizard',
|
||||
DataCollectionWizard = 'GetAzureRecommendationDialog'
|
||||
}
|
||||
|
||||
export enum TelemetryAction {
|
||||
@@ -44,6 +46,12 @@ export enum TelemetryAction {
|
||||
Next = 'next',
|
||||
Done = 'done',
|
||||
Cancel = 'cancel',
|
||||
OnPageLeave = 'OnPageLeave',
|
||||
GetMISkuRecommendation = 'GetMISkuRecommendation',
|
||||
GetVMSkuRecommendation = 'GetVMSkuRecommendation',
|
||||
GetInstanceRequirements = 'GetInstanceRequirements',
|
||||
StartDataCollection = 'StartDataCollection',
|
||||
StopDataCollection = 'StopDataCollection'
|
||||
}
|
||||
|
||||
export function logError(telemetryView: TelemetryViews, err: string, error: any): void {
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationStateModel, Page, StateChangeEvent } from '../models/stateMachine';
|
||||
import * as constants from '../constants/strings';
|
||||
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
|
||||
import { deepClone, findDropDownItemIndex, selectDropDownIndex } from '../api/utils';
|
||||
import { getSubscriptions } from '../api/azure';
|
||||
import * as styles from '../constants/styles';
|
||||
|
||||
export class AccountsSelectionPage extends MigrationWizardPage {
|
||||
private _azureAccountsDropdown!: azdata.DropDownComponent;
|
||||
private _accountTenantDropdown!: azdata.DropDownComponent;
|
||||
private _accountTenantFlexContainer!: azdata.FlexContainer;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
|
||||
super(wizard, azdata.window.createWizardPage(constants.ACCOUNTS_SELECTION_PAGE_TITLE), migrationStateModel);
|
||||
}
|
||||
|
||||
protected async registerContent(view: azdata.ModelView): Promise<void> {
|
||||
const pageDescription = {
|
||||
title: '',
|
||||
component: view.modelBuilder.text().withProps({
|
||||
value: constants.ACCOUNTS_SELECTION_PAGE_DESCRIPTION,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0',
|
||||
}
|
||||
}).component()
|
||||
};
|
||||
|
||||
this.wizard.customButtons[0].enabled = true;
|
||||
const form = view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
[
|
||||
pageDescription,
|
||||
await this.createAzureAccountsDropdown(view),
|
||||
await this.createAzureTenantContainer(view),
|
||||
]
|
||||
).withProps({
|
||||
CSSStyles: {
|
||||
'padding-top': '0'
|
||||
}
|
||||
}).component();
|
||||
await view.initializeModel(form);
|
||||
await this.populateAzureAccountsDropdown();
|
||||
this._disposables.push(view.onClosed(e =>
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } })));
|
||||
}
|
||||
|
||||
private createAzureAccountsDropdown(view: azdata.ModelView): azdata.FormComponent {
|
||||
|
||||
const azureAccountLabel = view.modelBuilder.text().withProps({
|
||||
value: constants.ACCOUNTS_SELECTION_PAGE_TITLE,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._azureAccountsDropdown = view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
ariaLabel: constants.ACCOUNTS_SELECTION_PAGE_TITLE,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
fireOnTextChange: true,
|
||||
})
|
||||
.withValidation((c) => {
|
||||
if (c.value) {
|
||||
if ((<azdata.CategoryValue>c.value)?.displayName === constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR) {
|
||||
this.wizard.message = {
|
||||
text: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR,
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
return false;
|
||||
}
|
||||
if (this.migrationStateModel._azureAccount?.isStale) {
|
||||
this.wizard.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: constants.ACCOUNT_STALE_ERROR(this.migrationStateModel._azureAccount)
|
||||
};
|
||||
return false;
|
||||
}
|
||||
this.wizard.message = {
|
||||
text: ''
|
||||
};
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}).component();
|
||||
|
||||
this._disposables.push(this._azureAccountsDropdown.onValueChanged(async (value) => {
|
||||
const selectedIndex = findDropDownItemIndex(this._azureAccountsDropdown, value);
|
||||
if (selectedIndex > -1) {
|
||||
const selectedAzureAccount = this.migrationStateModel.getAccount(selectedIndex);
|
||||
// Making a clone of the account object to preserve the original tenants
|
||||
this.migrationStateModel._azureAccount = deepClone(selectedAzureAccount);
|
||||
if (this.migrationStateModel._azureAccount.properties.tenants.length > 1) {
|
||||
this.migrationStateModel._accountTenants = selectedAzureAccount.properties.tenants;
|
||||
this._accountTenantDropdown.values = await this.migrationStateModel.getTenantValues();
|
||||
selectDropDownIndex(this._accountTenantDropdown, 0);
|
||||
await this._accountTenantFlexContainer.updateCssStyles({
|
||||
'display': 'inline'
|
||||
});
|
||||
} else {
|
||||
await this._accountTenantFlexContainer.updateCssStyles({
|
||||
'display': 'none'
|
||||
});
|
||||
if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.AzureAccount)) {
|
||||
(<azdata.CategoryValue[]>this._azureAccountsDropdown.values)?.forEach((account, index) => {
|
||||
if (account.name.toLowerCase() === this.migrationStateModel.savedInfo.azureAccount?.displayInfo.userId.toLowerCase()) {
|
||||
selectDropDownIndex(this._azureAccountsDropdown, index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
if (!(this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.Summary)) {
|
||||
this.migrationStateModel._subscriptions = undefined!;
|
||||
this.migrationStateModel._targetSubscription = undefined!;
|
||||
this.migrationStateModel._databaseBackup.subscription = undefined!;
|
||||
}
|
||||
await this._azureAccountsDropdown.validate();
|
||||
}
|
||||
}));
|
||||
|
||||
const linkAccountButton = view.modelBuilder.hyperlink()
|
||||
.withProps({
|
||||
label: constants.ACCOUNT_LINK_BUTTON_LABEL,
|
||||
url: '',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS
|
||||
}
|
||||
})
|
||||
.component();
|
||||
|
||||
this._disposables.push(linkAccountButton.onDidClick(async (event) => {
|
||||
await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount');
|
||||
await this.populateAzureAccountsDropdown();
|
||||
this.wizard.message = {
|
||||
text: ''
|
||||
};
|
||||
await this._azureAccountsDropdown.validate();
|
||||
}));
|
||||
|
||||
const flexContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column'
|
||||
})
|
||||
.withItems([
|
||||
azureAccountLabel,
|
||||
this._azureAccountsDropdown,
|
||||
linkAccountButton
|
||||
])
|
||||
.component();
|
||||
|
||||
return {
|
||||
title: '',
|
||||
component: flexContainer
|
||||
};
|
||||
}
|
||||
|
||||
private createAzureTenantContainer(view: azdata.ModelView): azdata.FormComponent {
|
||||
|
||||
const azureTenantDropdownLabel = view.modelBuilder.text().withProps({
|
||||
value: constants.AZURE_TENANT,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._accountTenantDropdown = view.modelBuilder.dropDown().withProps({
|
||||
ariaLabel: constants.AZURE_TENANT,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
fireOnTextChange: true,
|
||||
}).component();
|
||||
|
||||
this._disposables.push(this._accountTenantDropdown.onValueChanged(value => {
|
||||
/**
|
||||
* Replacing all the tenants in azure account with the tenant user has selected.
|
||||
* All azure requests will only run on this tenant from now on
|
||||
*/
|
||||
const selectedIndex = findDropDownItemIndex(this._accountTenantDropdown, value);
|
||||
const selectedTenant = this.migrationStateModel.getTenant(selectedIndex);
|
||||
this.migrationStateModel._azureTenant = deepClone(selectedTenant);
|
||||
if (selectedIndex > -1) {
|
||||
this.migrationStateModel._azureAccount.properties.tenants = [this.migrationStateModel.getTenant(selectedIndex)];
|
||||
this.migrationStateModel._subscriptions = undefined!;
|
||||
this.migrationStateModel._targetSubscription = undefined!;
|
||||
this.migrationStateModel._databaseBackup.subscription = undefined!;
|
||||
}
|
||||
|
||||
}));
|
||||
|
||||
this._accountTenantFlexContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column'
|
||||
})
|
||||
.withItems([
|
||||
azureTenantDropdownLabel,
|
||||
this._accountTenantDropdown
|
||||
])
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'display': 'none'
|
||||
}
|
||||
})
|
||||
.component();
|
||||
|
||||
return {
|
||||
title: '',
|
||||
component: this._accountTenantFlexContainer
|
||||
};
|
||||
}
|
||||
|
||||
private async populateAzureAccountsDropdown(): Promise<void> {
|
||||
this._azureAccountsDropdown.loading = true;
|
||||
try {
|
||||
this._azureAccountsDropdown.values = await this.migrationStateModel.getAccountValues();
|
||||
} finally {
|
||||
this._azureAccountsDropdown.loading = false;
|
||||
}
|
||||
|
||||
selectDropDownIndex(this._azureAccountsDropdown, 0);
|
||||
}
|
||||
|
||||
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
|
||||
this.wizard.registerNavigationValidator(async pageChangeInfo => {
|
||||
try {
|
||||
this.wizard.message = { text: '', };
|
||||
|
||||
if (this.migrationStateModel._azureAccount && !this.migrationStateModel._azureAccount?.isStale) {
|
||||
const subscriptions = await getSubscriptions(this.migrationStateModel._azureAccount);
|
||||
if (subscriptions?.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
this.wizard.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: constants.ACCOUNT_STALE_ERROR(this.migrationStateModel._azureAccount),
|
||||
};
|
||||
} catch (error) {
|
||||
this.wizard.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: constants.ACCOUNT_ACCESS_ERROR(this.migrationStateModel._azureAccount, error),
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
|
||||
}
|
||||
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
|
||||
super(wizard, azdata.window.createWizardPage(constants.SOURCE_CONFIGURATION, 'MigrationModePage'), migrationStateModel);
|
||||
super(wizard, azdata.window.createWizardPage(constants.DATABASE_FOR_ASSESSMENT_PAGE_TITLE), migrationStateModel);
|
||||
}
|
||||
|
||||
protected async registerContent(view: azdata.ModelView): Promise<void> {
|
||||
@@ -87,6 +87,7 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public async onPageLeave(): Promise<void> {
|
||||
const assessedDatabases = this.migrationStateModel._databaseAssessment ?? [];
|
||||
const selectedDatabases = this.selectedDbs();
|
||||
@@ -202,16 +203,8 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
|
||||
this._dbNames.push(finalResult[index].options.name);
|
||||
}
|
||||
|
||||
const title = this._view.modelBuilder.text().withProps({
|
||||
value: constants.DATABASE_FOR_MIGRATION,
|
||||
CSSStyles: {
|
||||
...styles.PAGE_TITLE_CSS,
|
||||
'margin-bottom': '8px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const text = this._view.modelBuilder.text().withProps({
|
||||
value: constants.DATABASE_MIGRATE_TEXT,
|
||||
value: constants.DATABASE_FOR_ASSESSMENT_DESCRIPTION,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS
|
||||
}
|
||||
@@ -284,14 +277,12 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
|
||||
const dbName = row[1].value as string;
|
||||
if (dbName?.toLowerCase() === sourceDatabaseName?.toLowerCase()) {
|
||||
row[0].value = true;
|
||||
} else {
|
||||
row[0].enabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
await this._databaseSelectorTable.setDataValues(this._databaseTableValues);
|
||||
await this.updateValuesOnSelection();
|
||||
}
|
||||
await this.updateValuesOnSelection();
|
||||
|
||||
this._disposables.push(this._databaseSelectorTable.onDataChanged(async () => {
|
||||
await this.updateValuesOnSelection();
|
||||
@@ -304,7 +295,6 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
|
||||
'margin': '0px 28px 0px 28px'
|
||||
}
|
||||
}).component();
|
||||
flex.addItem(title, { flex: '0 0 auto' });
|
||||
flex.addItem(text, { flex: '0 0 auto' });
|
||||
flex.addItem(this.createSearchComponent(), { flex: '0 0 auto' });
|
||||
flex.addItem(this._dbCount, { flex: '0 0 auto' });
|
||||
@@ -315,7 +305,7 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
|
||||
|
||||
public selectedDbs(): string[] {
|
||||
let result: string[] = [];
|
||||
this._databaseSelectorTable.dataValues?.forEach((arr, index) => {
|
||||
this._databaseSelectorTable?.dataValues?.forEach((arr, index) => {
|
||||
if (arr[0].value === true) {
|
||||
result.push(this._dbNames[index]);
|
||||
}
|
||||
|
||||
@@ -50,14 +50,6 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
|
||||
this._dmsInfoContainer = this._view.modelBuilder.flexContainer().withItems([
|
||||
this._statusLoadingComponent
|
||||
]).component();
|
||||
const dmsPortalInfo = this._view.modelBuilder.infoBox().withProps({
|
||||
text: constants.DMS_PORTAL_INFO,
|
||||
style: 'information',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS
|
||||
},
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH
|
||||
}).component();
|
||||
|
||||
const form = view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
@@ -65,9 +57,6 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
|
||||
{
|
||||
component: this.migrationServiceDropdownContainer()
|
||||
},
|
||||
{
|
||||
component: dmsPortalInfo
|
||||
},
|
||||
{
|
||||
component: this._dmsInfoContainer
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -113,8 +113,8 @@ export class SummaryPage extends MigrationWizardPage {
|
||||
await createHeadingTextComponent(this._view, constants.SOURCE_DATABASES),
|
||||
targetDatabaseRow,
|
||||
|
||||
await createHeadingTextComponent(this._view, constants.SKU_RECOMMENDATION_PAGE_TITLE),
|
||||
createInformationRow(this._view, constants.SKU_RECOMMENDATION_PAGE_TITLE, (this.migrationStateModel._targetType === MigrationTargetType.SQLVM) ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE),
|
||||
await createHeadingTextComponent(this._view, constants.AZURE_SQL_TARGET_PAGE_TITLE),
|
||||
createInformationRow(this._view, constants.AZURE_SQL_TARGET_PAGE_TITLE, (this.migrationStateModel._targetType === MigrationTargetType.SQLVM) ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE),
|
||||
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name),
|
||||
createInformationRow(this._view, constants.LOCATION, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.location)),
|
||||
createInformationRow(this._view, constants.RESOURCE_GROUP, getResourceGroupFromId(this.migrationStateModel._targetServerInstance.id)),
|
||||
|
||||
560
extensions/sql-migration/src/wizard/targetSelectionPage.ts
Normal file
560
extensions/sql-migration/src/wizard/targetSelectionPage.ts
Normal file
@@ -0,0 +1,560 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { EOL } from 'os';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationStateModel, MigrationTargetType, Page, StateChangeEvent } from '../models/stateMachine';
|
||||
import * as constants from '../constants/strings';
|
||||
import * as styles from '../constants/styles';
|
||||
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
|
||||
import { deepClone, findDropDownItemIndex, selectDropDownIndex } from '../api/utils';
|
||||
import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../telemtery';
|
||||
|
||||
export class TargetSelectionPage extends MigrationWizardPage {
|
||||
private _view!: azdata.ModelView;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
private _pageDescription!: azdata.TextComponent;
|
||||
private _azureAccountsDropdown!: azdata.DropDownComponent;
|
||||
private _accountTenantDropdown!: azdata.DropDownComponent;
|
||||
private _accountTenantFlexContainer!: azdata.FlexContainer;
|
||||
private _azureSubscriptionDropdown!: azdata.DropDownComponent;
|
||||
private _azureLocationDropdown!: azdata.DropDownComponent;
|
||||
private _azureResourceGroupDropdown!: azdata.DropDownComponent;
|
||||
private _azureResourceDropdownLabel!: azdata.TextComponent;
|
||||
private _azureResourceDropdown!: azdata.DropDownComponent;
|
||||
|
||||
|
||||
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
|
||||
super(wizard, azdata.window.createWizardPage(constants.AZURE_SQL_TARGET_PAGE_TITLE), migrationStateModel);
|
||||
}
|
||||
|
||||
protected async registerContent(view: azdata.ModelView): Promise<void> {
|
||||
this._view = view;
|
||||
|
||||
this._pageDescription = this._view.modelBuilder.text().withProps({
|
||||
value: constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(),
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const form = this._view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
[
|
||||
{
|
||||
component: this._pageDescription
|
||||
},
|
||||
{
|
||||
component: this.createAzureAccountsDropdown()
|
||||
},
|
||||
{
|
||||
component: this.createAzureTenantContainer()
|
||||
},
|
||||
{
|
||||
component: this.createTargetDropdownContainer()
|
||||
}
|
||||
]
|
||||
).withProps({
|
||||
CSSStyles: {
|
||||
'padding-top': '0'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._disposables.push(this._view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
await this._view.initializeModel(form);
|
||||
}
|
||||
|
||||
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
|
||||
|
||||
switch (this.migrationStateModel._targetType) {
|
||||
case MigrationTargetType.SQLMI:
|
||||
this._pageDescription.value = constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(constants.SKU_RECOMMENDATION_MI_CARD_TEXT);
|
||||
this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLVM:
|
||||
this._pageDescription.value = constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(constants.SKU_RECOMMENDATION_VM_CARD_TEXT);
|
||||
this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
|
||||
break;
|
||||
}
|
||||
|
||||
await this.populateAzureAccountsDropdown();
|
||||
|
||||
this.wizard.registerNavigationValidator((pageChangeInfo) => {
|
||||
const errors: string[] = [];
|
||||
this.wizard.message = {
|
||||
text: '',
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
|
||||
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((<azdata.CategoryValue>this._azureSubscriptionDropdown.value)?.displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
|
||||
errors.push(constants.INVALID_SUBSCRIPTION_ERROR);
|
||||
}
|
||||
if ((<azdata.CategoryValue>this._azureLocationDropdown.value)?.displayName === constants.NO_LOCATION_FOUND) {
|
||||
errors.push(constants.INVALID_LOCATION_ERROR);
|
||||
}
|
||||
if ((<azdata.CategoryValue>this._azureResourceGroupDropdown.value)?.displayName === constants.RESOURCE_GROUP_NOT_FOUND) {
|
||||
errors.push(constants.INVALID_RESOURCE_GROUP_ERROR);
|
||||
}
|
||||
|
||||
const resourceDropdownValue = (<azdata.CategoryValue>this._azureResourceDropdown.value)?.displayName;
|
||||
if (resourceDropdownValue === constants.NO_MANAGED_INSTANCE_FOUND) {
|
||||
errors.push(constants.INVALID_MANAGED_INSTANCE_ERROR);
|
||||
}
|
||||
else if (resourceDropdownValue === constants.NO_VIRTUAL_MACHINE_FOUND) {
|
||||
errors.push(constants.INVALID_VIRTUAL_MACHINE_ERROR);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
this.wizard.message = {
|
||||
text: errors.join(EOL),
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
|
||||
this.wizard.registerNavigationValidator((e) => {
|
||||
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.MigrationWizardTargetSelectionPage,
|
||||
TelemetryAction.OnPageLeave,
|
||||
{
|
||||
'sessionId': this.migrationStateModel?._sessionId,
|
||||
'subscriptionId': this.migrationStateModel?._targetSubscription?.id,
|
||||
'resourceGroup': this.migrationStateModel?._resourceGroup?.name,
|
||||
'tenantId': this.migrationStateModel?._azureTenant?.id || this.migrationStateModel?._azureAccount?.properties?.tenants[0]?.id
|
||||
}, {});
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
}
|
||||
|
||||
private createAzureAccountsDropdown(): azdata.FlexContainer {
|
||||
const azureAccountLabel = this._view.modelBuilder.text().withProps({
|
||||
value: constants.ACCOUNTS_SELECTION_PAGE_TITLE,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin-top': '-1em'
|
||||
}
|
||||
}).component();
|
||||
this._azureAccountsDropdown = this._view.modelBuilder.dropDown().withProps({
|
||||
ariaLabel: constants.ACCOUNTS_SELECTION_PAGE_TITLE,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
required: true,
|
||||
CSSStyles: {
|
||||
'margin-top': '-1em'
|
||||
},
|
||||
}).component();
|
||||
this._disposables.push(this._azureAccountsDropdown.onValueChanged(async (value) => {
|
||||
const selectedIndex = findDropDownItemIndex(this._azureAccountsDropdown, value);
|
||||
if (selectedIndex > -1) {
|
||||
const selectedAzureAccount = this.migrationStateModel.getAccount(selectedIndex);
|
||||
// Making a clone of the account object to preserve the original tenants
|
||||
this.migrationStateModel._azureAccount = deepClone(selectedAzureAccount);
|
||||
if (this.migrationStateModel._azureAccount.properties.tenants.length > 1) {
|
||||
this.migrationStateModel._accountTenants = selectedAzureAccount.properties.tenants;
|
||||
this._accountTenantDropdown.values = await this.migrationStateModel.getTenantValues();
|
||||
selectDropDownIndex(this._accountTenantDropdown, 0);
|
||||
await this._accountTenantFlexContainer.updateCssStyles({
|
||||
'display': 'inline'
|
||||
});
|
||||
} else {
|
||||
await this._accountTenantFlexContainer.updateCssStyles({
|
||||
'display': 'none'
|
||||
});
|
||||
}
|
||||
await this._azureAccountsDropdown.validate();
|
||||
await this.populateSubscriptionDropdown();
|
||||
}
|
||||
}));
|
||||
|
||||
const linkAccountButton = this._view.modelBuilder.hyperlink()
|
||||
.withProps({
|
||||
label: constants.ACCOUNT_LINK_BUTTON_LABEL,
|
||||
url: '',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS
|
||||
}
|
||||
})
|
||||
.component();
|
||||
|
||||
this._disposables.push(linkAccountButton.onDidClick(async (event) => {
|
||||
await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount');
|
||||
await this.populateAzureAccountsDropdown();
|
||||
this.wizard.message = {
|
||||
text: ''
|
||||
};
|
||||
await this._azureAccountsDropdown.validate();
|
||||
}));
|
||||
|
||||
const flexContainer = this._view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column'
|
||||
})
|
||||
.withItems([
|
||||
azureAccountLabel,
|
||||
this._azureAccountsDropdown,
|
||||
linkAccountButton
|
||||
])
|
||||
.component();
|
||||
return flexContainer;
|
||||
}
|
||||
|
||||
private createAzureTenantContainer(): azdata.FlexContainer {
|
||||
const azureTenantDropdownLabel = this._view.modelBuilder.text().withProps({
|
||||
value: constants.AZURE_TENANT,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._accountTenantDropdown = this._view.modelBuilder.dropDown().withProps({
|
||||
ariaLabel: constants.AZURE_TENANT,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
fireOnTextChange: true,
|
||||
}).component();
|
||||
|
||||
this._disposables.push(this._accountTenantDropdown.onValueChanged(value => {
|
||||
/**
|
||||
* Replacing all the tenants in azure account with the tenant user has selected.
|
||||
* All azure requests will only run on this tenant from now on
|
||||
*/
|
||||
const selectedIndex = findDropDownItemIndex(this._accountTenantDropdown, value);
|
||||
const selectedTenant = this.migrationStateModel.getTenant(selectedIndex);
|
||||
this.migrationStateModel._azureTenant = deepClone(selectedTenant);
|
||||
if (selectedIndex > -1) {
|
||||
this.migrationStateModel._azureAccount.properties.tenants = [this.migrationStateModel.getTenant(selectedIndex)];
|
||||
this.migrationStateModel._subscriptions = undefined!;
|
||||
this.migrationStateModel._targetSubscription = undefined!;
|
||||
this.migrationStateModel._databaseBackup.subscription = undefined!;
|
||||
}
|
||||
|
||||
}));
|
||||
|
||||
this._accountTenantFlexContainer = this._view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column'
|
||||
})
|
||||
.withItems([
|
||||
azureTenantDropdownLabel,
|
||||
this._accountTenantDropdown
|
||||
])
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'display': 'none'
|
||||
}
|
||||
})
|
||||
.component();
|
||||
return this._accountTenantFlexContainer;
|
||||
}
|
||||
|
||||
private createTargetDropdownContainer(): azdata.FlexContainer {
|
||||
const subscriptionDropdownLabel = this._view.modelBuilder.text().withProps({
|
||||
value: constants.SUBSCRIPTION,
|
||||
description: constants.TARGET_SUBSCRIPTION_INFO,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
}
|
||||
}).component();
|
||||
this._azureSubscriptionDropdown = this._view.modelBuilder.dropDown().withProps({
|
||||
ariaLabel: constants.SUBSCRIPTION,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
CSSStyles: {
|
||||
'margin-top': '-1em'
|
||||
},
|
||||
}).component();
|
||||
this._disposables.push(this._azureSubscriptionDropdown.onValueChanged(async (value) => {
|
||||
const selectedIndex = findDropDownItemIndex(this._azureSubscriptionDropdown, value);
|
||||
if (selectedIndex > -1 &&
|
||||
value !== constants.NO_SUBSCRIPTIONS_FOUND) {
|
||||
this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(selectedIndex);
|
||||
this.migrationStateModel._targetServerInstance = undefined!;
|
||||
this.migrationStateModel._sqlMigrationService = undefined!;
|
||||
await this.populateLocationDropdown();
|
||||
}
|
||||
}));
|
||||
|
||||
const azureLocationLabel = this._view.modelBuilder.text().withProps({
|
||||
value: constants.LOCATION,
|
||||
description: constants.TARGET_LOCATION_INFO,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS
|
||||
}
|
||||
}).component();
|
||||
this._azureLocationDropdown = this._view.modelBuilder.dropDown().withProps({
|
||||
ariaLabel: constants.LOCATION,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
CSSStyles: {
|
||||
'margin-top': '-1em'
|
||||
},
|
||||
}).component();
|
||||
this._disposables.push(this._azureLocationDropdown.onValueChanged(async (value) => {
|
||||
const selectedIndex = findDropDownItemIndex(this._azureLocationDropdown, value);
|
||||
if (selectedIndex > -1 &&
|
||||
value !== constants.NO_LOCATION_FOUND) {
|
||||
this.migrationStateModel._location = this.migrationStateModel.getLocation(selectedIndex);
|
||||
await this.populateResourceGroupDropdown();
|
||||
}
|
||||
}));
|
||||
|
||||
const azureResourceGroupLabel = this._view.modelBuilder.text().withProps({
|
||||
value: constants.RESOURCE_GROUP,
|
||||
description: constants.TARGET_RESOURCE_GROUP_INFO,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS
|
||||
}
|
||||
}).component();
|
||||
this._azureResourceGroupDropdown = this._view.modelBuilder.dropDown().withProps({
|
||||
ariaLabel: constants.RESOURCE_GROUP,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
CSSStyles: {
|
||||
'margin-top': '-1em'
|
||||
},
|
||||
}).component();
|
||||
this._disposables.push(this._azureResourceGroupDropdown.onValueChanged(async (value) => {
|
||||
const selectedIndex = findDropDownItemIndex(this._azureResourceGroupDropdown, value);
|
||||
if (selectedIndex > -1) {
|
||||
if (value !== constants.RESOURCE_GROUP_NOT_FOUND) {
|
||||
this.migrationStateModel._resourceGroup = this.migrationStateModel.getAzureResourceGroup(selectedIndex);
|
||||
}
|
||||
await this.populateResourceInstanceDropdown();
|
||||
}
|
||||
}));
|
||||
|
||||
this._azureResourceDropdownLabel = this._view.modelBuilder.text().withProps({
|
||||
value: constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE,
|
||||
description: constants.TARGET_RESOURCE_INFO,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS
|
||||
}
|
||||
}).component();
|
||||
this._azureResourceDropdown = this._view.modelBuilder.dropDown().withProps({
|
||||
ariaLabel: constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
CSSStyles: {
|
||||
'margin-top': '-1em'
|
||||
},
|
||||
}).component();
|
||||
this._disposables.push(this._azureResourceDropdown.onValueChanged(value => {
|
||||
const selectedIndex = findDropDownItemIndex(this._azureResourceDropdown, value);
|
||||
if (selectedIndex > -1 &&
|
||||
value !== constants.NO_MANAGED_INSTANCE_FOUND &&
|
||||
value !== constants.NO_VIRTUAL_MACHINE_FOUND) {
|
||||
this.migrationStateModel._sqlMigrationServices = undefined!;
|
||||
|
||||
switch (this.migrationStateModel._targetType) {
|
||||
case MigrationTargetType.SQLVM:
|
||||
this.migrationStateModel._targetServerInstance = this.migrationStateModel.getVirtualMachine(selectedIndex);
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLMI:
|
||||
this.migrationStateModel._targetServerInstance = this.migrationStateModel.getManagedInstance(selectedIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
return this._view.modelBuilder.flexContainer().withItems(
|
||||
[
|
||||
subscriptionDropdownLabel,
|
||||
this._azureSubscriptionDropdown,
|
||||
azureLocationLabel,
|
||||
this._azureLocationDropdown,
|
||||
azureResourceGroupLabel,
|
||||
this._azureResourceGroupDropdown,
|
||||
this._azureResourceDropdownLabel,
|
||||
this._azureResourceDropdown
|
||||
]
|
||||
).withLayout({
|
||||
flexFlow: 'column',
|
||||
}).component();
|
||||
}
|
||||
|
||||
private async populateAzureAccountsDropdown(): Promise<void> {
|
||||
try {
|
||||
this._azureAccountsDropdown.loading = true;
|
||||
this._azureSubscriptionDropdown.loading = true;
|
||||
this._azureLocationDropdown.loading = true;
|
||||
this._azureResourceGroupDropdown.loading = true;
|
||||
this._azureResourceDropdown.loading = true;
|
||||
|
||||
this._azureAccountsDropdown.values = await this.migrationStateModel.getAccountValues();
|
||||
|
||||
if (this.hasSavedInfo() && this._azureAccountsDropdown.values) {
|
||||
(<azdata.CategoryValue[]>this._azureAccountsDropdown.values)?.forEach((account, index) => {
|
||||
if ((<azdata.CategoryValue>account).name.toLowerCase() === this.migrationStateModel.savedInfo.azureAccount?.displayInfo.userId.toLowerCase()) {
|
||||
selectDropDownIndex(this._azureAccountsDropdown, index);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
selectDropDownIndex(this._azureAccountsDropdown, 0);
|
||||
}
|
||||
} finally {
|
||||
this._azureAccountsDropdown.loading = false;
|
||||
this._azureSubscriptionDropdown.loading = false;
|
||||
this._azureLocationDropdown.loading = false;
|
||||
this._azureResourceGroupDropdown.loading = false;
|
||||
this._azureResourceDropdown.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async populateSubscriptionDropdown(): Promise<void> {
|
||||
try {
|
||||
this._azureSubscriptionDropdown.loading = true;
|
||||
this._azureLocationDropdown.loading = true;
|
||||
this._azureResourceGroupDropdown.loading = true;
|
||||
this._azureResourceDropdown.loading = true;
|
||||
|
||||
this._azureSubscriptionDropdown.values = await this.migrationStateModel.getSubscriptionsDropdownValues();
|
||||
if (this.hasSavedInfo() && this._azureSubscriptionDropdown.values) {
|
||||
this._azureSubscriptionDropdown.values!.forEach((subscription, index) => {
|
||||
if ((<azdata.CategoryValue>subscription).name.toLowerCase() === this.migrationStateModel.savedInfo?.subscription?.id.toLowerCase()) {
|
||||
selectDropDownIndex(this._azureSubscriptionDropdown, index);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
selectDropDownIndex(this._azureSubscriptionDropdown, 0);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
this._azureSubscriptionDropdown.loading = false;
|
||||
this._azureLocationDropdown.loading = false;
|
||||
this._azureResourceGroupDropdown.loading = false;
|
||||
this._azureResourceDropdown.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async populateLocationDropdown(): Promise<void> {
|
||||
try {
|
||||
this._azureLocationDropdown.loading = true;
|
||||
this._azureResourceGroupDropdown.loading = true;
|
||||
this._azureResourceDropdown.loading = true;
|
||||
|
||||
this._azureLocationDropdown.values = await this.migrationStateModel.getAzureLocationDropdownValues(this.migrationStateModel._targetSubscription);
|
||||
if (this.hasSavedInfo() && this._azureLocationDropdown.values) {
|
||||
this._azureLocationDropdown.values.forEach((location, index) => {
|
||||
if ((<azdata.CategoryValue>location)?.displayName.toLowerCase() === this.migrationStateModel.savedInfo?.location?.displayName.toLowerCase()) {
|
||||
selectDropDownIndex(this._azureLocationDropdown, index);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
selectDropDownIndex(this._azureLocationDropdown, 0);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
this._azureLocationDropdown.loading = false;
|
||||
this._azureResourceGroupDropdown.loading = false;
|
||||
this._azureResourceDropdown.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async populateResourceGroupDropdown(): Promise<void> {
|
||||
try {
|
||||
this._azureResourceGroupDropdown.loading = true;
|
||||
this._azureResourceDropdown.loading = true;
|
||||
|
||||
this._azureResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._targetSubscription);
|
||||
if (this.hasSavedInfo() && this._azureResourceGroupDropdown.values) {
|
||||
this._azureResourceGroupDropdown.values.forEach((resourceGroup, index) => {
|
||||
if ((<azdata.CategoryValue>resourceGroup)?.name.toLowerCase() === this.migrationStateModel.savedInfo?.resourceGroup?.id.toLowerCase()) {
|
||||
selectDropDownIndex(this._azureResourceGroupDropdown, index);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
selectDropDownIndex(this._azureResourceGroupDropdown, 0);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
this._azureResourceGroupDropdown.loading = false;
|
||||
this._azureResourceDropdown.loading = false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private async populateResourceInstanceDropdown(): Promise<void> {
|
||||
try {
|
||||
this._azureResourceDropdown.loading = true;
|
||||
|
||||
switch (this.migrationStateModel._targetType) {
|
||||
case MigrationTargetType.SQLVM:
|
||||
this._azureResourceDropdown.values = await this.migrationStateModel.getSqlVirtualMachineValues(
|
||||
this.migrationStateModel._targetSubscription,
|
||||
this.migrationStateModel._location,
|
||||
this.migrationStateModel._resourceGroup);
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLMI:
|
||||
this._azureResourceDropdown.values = await this.migrationStateModel.getManagedInstanceValues(
|
||||
this.migrationStateModel._targetSubscription,
|
||||
this.migrationStateModel._location,
|
||||
this.migrationStateModel._resourceGroup);
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.hasSavedInfo() && this._azureResourceDropdown.values) {
|
||||
this._azureResourceDropdown.values.forEach((resource, index) => {
|
||||
if ((<azdata.CategoryValue>resource).name.toLowerCase() === this.migrationStateModel.savedInfo?.targetServerInstance?.name.toLowerCase()) {
|
||||
selectDropDownIndex(this._azureResourceDropdown, index);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
selectDropDownIndex(this._azureResourceDropdown, 0);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
this._azureResourceDropdown.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private hasSavedInfo(): boolean {
|
||||
return this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.TargetSelection);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import * as loc from '../constants/strings';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { SKURecommendationPage } from './skuRecommendationPage';
|
||||
import { DatabaseBackupPage } from './databaseBackupPage';
|
||||
import { AccountsSelectionPage } from './accountsSelectionPage';
|
||||
import { TargetSelectionPage } from './targetSelectionPage';
|
||||
import { IntergrationRuntimePage } from './integrationRuntimePage';
|
||||
import { SummaryPage } from './summaryPage';
|
||||
import { MigrationModePage } from './migrationModePage';
|
||||
@@ -41,18 +41,18 @@ export class WizardController {
|
||||
this._wizardObject.generateScriptButton.hidden = true;
|
||||
const saveAndCloseButton = azdata.window.createButton(loc.SAVE_AND_CLOSE);
|
||||
this._wizardObject.customButtons = [saveAndCloseButton];
|
||||
const skuRecommendationPage = new SKURecommendationPage(this._wizardObject, stateModel);
|
||||
const migrationModePage = new MigrationModePage(this._wizardObject, stateModel);
|
||||
const databaseSelectorPage = new DatabaseSelectorPage(this._wizardObject, stateModel);
|
||||
const azureAccountsPage = new AccountsSelectionPage(this._wizardObject, stateModel);
|
||||
const skuRecommendationPage = new SKURecommendationPage(this._wizardObject, stateModel);
|
||||
const targetSelectionPage = new TargetSelectionPage(this._wizardObject, stateModel);
|
||||
const migrationModePage = new MigrationModePage(this._wizardObject, stateModel);
|
||||
const databaseBackupPage = new DatabaseBackupPage(this._wizardObject, stateModel);
|
||||
const integrationRuntimePage = new IntergrationRuntimePage(this._wizardObject, stateModel);
|
||||
const summaryPage = new SummaryPage(this._wizardObject, stateModel);
|
||||
|
||||
const pages: MigrationWizardPage[] = [
|
||||
azureAccountsPage,
|
||||
databaseSelectorPage,
|
||||
skuRecommendationPage,
|
||||
targetSelectionPage,
|
||||
migrationModePage,
|
||||
databaseBackupPage,
|
||||
integrationRuntimePage,
|
||||
@@ -61,6 +61,13 @@ export class WizardController {
|
||||
|
||||
this._wizardObject.pages = pages.map(p => p.getwizardPage());
|
||||
|
||||
// kill existing data collection if user relaunches the wizard via new migration or retry existing migration
|
||||
await this._model.refreshPerfDataCollection();
|
||||
if ((!this._model.resumeAssessment || this._model.retryMigration) && this._model._perfDataCollectionIsCollecting) {
|
||||
void this._model.stopPerfDataCollection();
|
||||
void vscode.window.showInformationMessage(loc.AZURE_RECOMMENDATION_STOP_POPUP);
|
||||
}
|
||||
|
||||
const wizardSetupPromises: Thenable<void>[] = [];
|
||||
wizardSetupPromises.push(...pages.map(p => p.registerWizardContent()));
|
||||
wizardSetupPromises.push(this._wizardObject.open());
|
||||
@@ -68,6 +75,7 @@ export class WizardController {
|
||||
if (this._model.savedInfo.closedPage >= Page.MigrationMode) {
|
||||
this._model.refreshDatabaseBackupPage = true;
|
||||
}
|
||||
|
||||
// if the user selected network share and selected save & close afterwards, it should always return to the database backup page so that
|
||||
// the user can input their password again
|
||||
if (this._model.savedInfo.closedPage >= Page.DatabaseBackup && this._model.savedInfo.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
|
||||
@@ -106,6 +114,10 @@ export class WizardController {
|
||||
saveAndCloseButton.onClick(async () => {
|
||||
await stateModel.saveInfo(serverName, this._wizardObject.currentPage);
|
||||
await this._wizardObject.close();
|
||||
|
||||
if (stateModel.performanceCollectionInProgress()) {
|
||||
void vscode.window.showInformationMessage(loc.SAVE_AND_CLOSE_POPUP);
|
||||
}
|
||||
});
|
||||
|
||||
this._wizardObject.cancelButton.onClick(e => {
|
||||
|
||||
Reference in New Issue
Block a user