Initial work on Arc tree view (#11008)

* Initial work on Arc tree view

* finish my thoughts
This commit is contained in:
Charles Gagnon
2020-06-18 16:50:31 -07:00
committed by GitHub
parent 935733d23c
commit 88fce764d3
23 changed files with 464 additions and 77 deletions

View File

@@ -1 +0,0 @@
<svg width="26" height="26" xmlns="http://www.w3.org/2000/svg"> <g id="svg_1"> <polygon transform="rotate(90 12.967000007629395,8.000000000000002) " fill="#ffffff" points="7.549000024795532,-4.9200000166893005 5.464999999850988,-2.8359999656677246 16.300999641418457,8 5.464999999850988,18.836000442504883 7.549000024795532,20.920000076293945 20.4689998626709,8 " id="svg_2"/> <polygon transform="rotate(90 12.96700096130371,18.90800476074219) " fill="#ffffff" points="7.5490007400512695,5.988004416227341 5.465001106262207,8.07200500369072 16.301002502441406,18.908005446195602 5.465001106262207,29.744003981351852 7.5490007400512695,31.828003615140915 20.46900177001953,18.908005446195602 " id="svg_3"/> </g> </svg>

Before

Width:  |  Height:  |  Size: 717 B

View File

@@ -1 +0,0 @@
<svg width="26" height="26" xmlns="http://www.w3.org/2000/svg"> <g transform="rotate(90 12.967000007629395,13.500000000000002) " id="svg_1"> <polygon id="svg_2" points="2.049,0.58 -0.035,2.664 10.801,13.5 -0.035,24.336 2.049,26.42 14.969,13.5 " fill="#231F20"/> <polygon id="svg_3" points="13.049,0.58 10.965,2.664 21.801,13.5 10.965,24.336 13.049,26.42 25.969,13.5 " fill="#231F20"/> </g> </svg>

Before

Width:  |  Height:  |  Size: 398 B

View File

@@ -1 +0,0 @@
<svg width="26" height="26" xmlns="http://www.w3.org/2000/svg"> <g id="svg_1"> <polygon transform="rotate(-90 12.718243598937994,19.000000000000004) " fill="#ffffff" points="7.300243854522705,6.079999923706055 5.216243773698807,8.164000034332275 16.05224323272705,19 5.216243773698807,29.836000442504883 7.300243854522705,31.920000076293945 20.220243453979492,19 " id="svg_2"/> <polygon transform="rotate(-90 12.718244552612306,8.000000953674316) " fill="#ffffff" points="7.300244331359863,-4.919999122619629 5.216244697570801,-2.835999011993408 16.052245140075684,8.000000953674316 5.216244697570801,18.8360013961792 7.300244331359863,20.92000102996826 20.22024440765381,8.000000953674316 " id="svg_3"/> </g> </svg>

Before

Width:  |  Height:  |  Size: 716 B

View File

@@ -1 +0,0 @@
<svg width="26" height="26" xmlns="http://www.w3.org/2000/svg"> <g transform="rotate(-90 12.967000007629396,13.5) " id="svg_1"> <polygon id="svg_2" points="2.049,0.58 -0.035,2.664 10.801,13.5 -0.035,24.336 2.049,26.42 14.969,13.5 " fill="#231F20"/> <polygon id="svg_3" points="13.049,0.58 10.965,2.664 21.801,13.5 10.965,24.336 13.049,26.42 25.969,13.5 " fill="#231F20"/> </g> </svg>

Before

Width:  |  Height:  |  Size: 385 B

View File

@@ -0,0 +1,23 @@
<svg id="b5f5c9b0-d3fa-415d-8540-6bd32b8a5433" data-name="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="18" height="18" viewBox="0 0 18 18">
<defs>
<linearGradient id="a42db308-c624-4092-b558-087b84167065" x1="3.427" y1="263.506" x2="14.573" y2="263.506" gradientTransform="matrix(1, 0, 0, -1, 0, 272)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#005ba1"/>
<stop offset="0.07" stop-color="#0060a9"/>
<stop offset="0.36" stop-color="#0071c8"/>
<stop offset="0.52" stop-color="#0078d4"/>
<stop offset="0.64" stop-color="#0074cd"/>
<stop offset="0.82" stop-color="#006abb"/>
<stop offset="1" stop-color="#005ba1"/>
</linearGradient>
</defs>
<title>Data_controller</title>
<path d="M17.548,15.194c-.16.7-1.035,1.391-2.617,1.929a21.585,21.585,0,0,1-12.125.017C1.349,16.626.563,15.97.44,15.3c-.022-.118,0-1.966,0-1.966l17.136-.16S17.568,15.109,17.548,15.194Z" fill="#5ea0ef"/>
<ellipse cx="9" cy="13.294" rx="8.576" ry="2.965" transform="translate(-0.133 0.091) rotate(-0.575)" fill="#50e6ff"/>
<g>
<path d="M9,4.129c-3.078,0-5.573-.869-5.573-2.017V12.859c0,1.1,2.452,2,5.5,2.017H9c3.078,0,5.573-.87,5.573-2.017V2.112C14.573,3.234,12.078,4.129,9,4.129Z" fill="url(#a42db308-c624-4092-b558-087b84167065)"/>
<path d="M14.573,2.112c0,1.122-2.5,2.017-5.573,2.017S3.427,3.26,3.427,2.112,5.922.1,9,.1s5.573.87,5.573,2.017" fill="#e8e8e8"/>
<path d="M13.278,1.947c0,.713-1.922,1.287-4.278,1.287S4.722,2.66,4.722,1.947,6.644.669,9,.669s4.278.574,4.278,1.278" fill="#50e6ff"/>
<path d="M9,2.269a10.032,10.032,0,0,0-3.382.495A9.92,9.92,0,0,0,9,3.234a9.711,9.711,0,0,0,3.382-.5A10.279,10.279,0,0,0,9,2.269Z" fill="#198ab3"/>
</g>
<path d="M12.039,9.808v-.69h-.1l-.74-.24-.19-.47.37-.8-.48-.48-.1.05-.76.31-.48-.19-.3-.84h-.65v.1l-.24.74-.47.19-.86-.38-.48.49.05.09.43.69-.2.47-.88.36v.68h.09l.74.24.19.47-.37.8.48.48h.1l.69-.35.47.19.3.83h.69v-.09l.24-.74.47-.19.8.37.48-.48-.05-.1-.35-.69.19-.47Zm-3,1a1.33,1.33,0,1,1,1.32-1.34v.01A1.322,1.322,0,0,1,9.05,10.81l-.071,0Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -14,7 +14,8 @@
"activationEvents": [
"onCommand:arc.manageArcController",
"onCommand:arc.manageMiaa",
"onCommand:arc.managePostgres"
"onCommand:arc.managePostgres",
"onView:azureArc"
],
"repository": {
"type": "git",
@@ -22,6 +23,14 @@
},
"main": "./out/extension",
"contributes": {
"dataExplorer": {
"azureArc": [
{
"id": "azureArc",
"name": "%arc.view.title%"
}
]
},
"commands": [
{
"command": "arc.manageArcController",
@@ -34,6 +43,19 @@
{
"command": "arc.managePostgres",
"title": "%arc.managePostgres%"
},
{
"command": "arc.openDashboard",
"title": "%arc.openDashboard%"
},
{
"command": "arc.addController",
"title": "%command.addController.title%",
"icon": "$(add)"
},
{
"command": "arc.removeController",
"title": "%command.removeController.title%"
}
],
"menus": {
@@ -49,6 +71,33 @@
{
"command": "arc.managePostgres",
"when": "false"
},
{
"command": "arc.openDashboard",
"when": "false"
},
{
"command": "arc.removeController",
"when": "false"
}
],
"view/title": [
{
"command": "arc.addController",
"when": "view == azureArc",
"group": "navigation"
}
],
"view/item/context": [
{
"command": "arc.openDashboard",
"when": "view == azureArc && viewItem != loading",
"group": "navigation@1"
},
{
"command": "arc.removeController",
"when": "view == azureArc && viewItem == dataControllers",
"group": "navigation@2"
}
]
},

View File

@@ -5,5 +5,9 @@
"arc.ignoreSslVerification.desc" : "Ignore SSL verification errors against the controller endpoint if true",
"arc.manageMiaa": "Manage MIAA",
"arc.managePostgres": "Manage Postgres",
"arc.manageArcController": "Manage Arc Controller"
"arc.manageArcController": "Manage Arc Controller",
"arc.view.title" : "Azure Arc Controllers",
"command.addController.title": "Connect to Controller",
"command.removeController.title": "Remove Controller",
"arc.openDashboard": "Manage"
}

View File

@@ -46,16 +46,26 @@ export async function getAzurecoreApi(): Promise<azurecore.IExtension> {
return azurecoreApi;
}
export function getResourceTypeIcon(resourceType: string): IconPath | undefined {
/**
* Gets the IconPath for the specified resource type, or undefined if the type is unknown.
* @param resourceType The resource type
*/
export function getResourceTypeIcon(resourceType: string | undefined): IconPath | undefined {
switch (resourceType) {
case ResourceType.sqlManagedInstances:
return IconPathHelper.miaa;
case ResourceType.postgresInstances:
return IconPathHelper.postgres;
case ResourceType.dataControllers:
return IconPathHelper.controller;
}
return undefined;
}
/**
* Returns the text to display for known connection modes
* @param connectionMode The string repsenting the connection mode
*/
export function getConnectionModeDisplayText(connectionMode: string | undefined): string {
connectionMode = connectionMode ?? '';
switch (connectionMode) {
@@ -67,6 +77,30 @@ export function getConnectionModeDisplayText(connectionMode: string | undefined)
return connectionMode;
}
/**
* Gets the display text for the database state returned from querying the database.
* @param state The state value returned from the database
*/
export function getDatabaseStateDisplayText(state: string): string {
switch (state.toUpperCase()) {
case 'ONLINE':
return loc.online;
case 'OFFLINE':
return loc.offline;
case 'RESTORING':
return loc.restoring;
case 'RECOVERING':
return loc.recovering;
case 'RECOVERY PENDING ':
return loc.recoveryPending;
case 'SUSPECT':
return loc.suspect;
case 'EMERGENCY':
return loc.emergecy;
}
return state;
}
/**
* Opens an input box prompting the user to enter in the name of a resource to delete
* @param namespace The namespace of the resource to delete
@@ -79,12 +113,10 @@ export async function promptForResourceDeletion(namespace: string, name: string)
inputBox.placeholder = name;
return new Promise(resolve => {
let valueAccepted = false;
inputBox.show();
inputBox.onDidAccept(() => {
if (inputBox.value === name) {
valueAccepted = true;
inputBox.hide();
inputBox.dispose();
resolve(true);
} else {
inputBox.validationMessage = loc.invalidResourceDeletionName(inputBox.value);
@@ -94,10 +126,12 @@ export async function promptForResourceDeletion(namespace: string, name: string)
if (!valueAccepted) {
resolve(false);
}
inputBox.dispose();
});
inputBox.onDidChangeValue(() => {
inputBox.validationMessage = '';
});
inputBox.show();
});
}
@@ -105,13 +139,35 @@ export async function promptForResourceDeletion(namespace: string, name: string)
* Gets the message to display for a given error object that may be a variety of types.
* @param error The error object
*/
export function getErrorText(error: any): string {
export function getErrorMessage(error: any): string {
if (error?.body?.reason) {
// For HTTP Errors pull out the reason message since that's usually the most helpful
// For HTTP Errors with a body pull out the reason message since that's usually the most helpful
return error.body.reason;
} else if (error instanceof Error) {
} else if (error.message) {
if (error.response?.statusMessage) {
// Some Http errors just have a status message as additional detail, but it's not enough on its
// own to be useful so append to the message as well
return `${error.message} (${error.response.statusMessage})`;
}
return error.message;
} else {
return error;
}
}
/**
* Parses an instance name from the controller. An instance name will either be just its name
* e.g. myinstance or namespace_name e.g. mynamespace_my-instance.
* @param instanceName The instance name in one of the formats described
*/
export function parseInstanceName(instanceName: string | undefined): string {
instanceName = instanceName ?? '';
const parts: string[] = instanceName.split('_');
if (parts.length === 2) {
instanceName = parts[1];
}
else if (parts.length > 2) {
throw new Error(`Cannot parse resource '${instanceName}'. Acceptable formats are 'namespace_name' or 'name'.`);
}
return instanceName;
}

View File

@@ -31,6 +31,7 @@ export class IconPathHelper {
public static support: IconPath;
public static wrench: IconPath;
public static miaa: IconPath;
public static controller: IconPath;
public static setExtensionContext(context: vscode.ExtensionContext) {
IconPathHelper.context = context;
@@ -58,14 +59,6 @@ export class IconPathHelper {
light: IconPathHelper.context.asAbsolutePath('images/copy.svg'),
dark: IconPathHelper.context.asAbsolutePath('images/copy.svg')
};
IconPathHelper.collapseUp = {
light: IconPathHelper.context.asAbsolutePath('images/collapse-up.svg'),
dark: IconPathHelper.context.asAbsolutePath('images/collapse-up-inverse.svg')
};
IconPathHelper.collapseDown = {
light: IconPathHelper.context.asAbsolutePath('images/collapse-down.svg'),
dark: IconPathHelper.context.asAbsolutePath('images/collapse-down-inverse.svg')
};
IconPathHelper.postgres = {
light: IconPathHelper.context.asAbsolutePath('images/postgres.svg'),
dark: IconPathHelper.context.asAbsolutePath('images/postgres.svg')
@@ -106,6 +99,10 @@ export class IconPathHelper {
light: context.asAbsolutePath('images/miaa.svg'),
dark: context.asAbsolutePath('images/miaa.svg'),
};
IconPathHelper.controller = {
light: context.asAbsolutePath('images/data_controller.svg'),
dark: context.asAbsolutePath('images/data_controller.svg'),
};
}
}

View File

@@ -3,7 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as loc from './localizedConstants';
import { IconPathHelper } from './constants';
@@ -14,10 +13,33 @@ import { PostgresModel } from './models/postgresModel';
import { ControllerDashboard } from './ui/dashboards/controller/controllerDashboard';
import { MiaaDashboard } from './ui/dashboards/miaa/miaaDashboard';
import { MiaaModel } from './models/miaaModel';
import { AzureArcTreeDataProvider } from './ui/tree/controllerTreeDataProvider';
import { ControllerTreeNode } from './ui/tree/controllerTreeNode';
import { TreeNode } from './ui/tree/treeNode';
export async function activate(context: vscode.ExtensionContext): Promise<void> {
IconPathHelper.setExtensionContext(context);
const treeDataProvider = new AzureArcTreeDataProvider(context);
vscode.window.registerTreeDataProvider('azureArc', treeDataProvider);
vscode.commands.registerCommand('arc.addController', () => {
// Controller information
const controllerUrl = '';
const auth = new BasicAuth('', '');
const controllerModel = new ControllerModel(controllerUrl, auth);
treeDataProvider.addController(controllerModel);
});
vscode.commands.registerCommand('arc.removeController', (controllerNode: ControllerTreeNode) => {
treeDataProvider.removeController(controllerNode);
});
vscode.commands.registerCommand('arc.openDashboard', async (treeNode: TreeNode) => {
await treeNode.openDashboard().catch(err => vscode.window.showErrorMessage(loc.openDashboardFailed(err)));
});
vscode.commands.registerCommand('arc.manageArcController', async () => {
// Controller information
const controllerUrl = '';
@@ -40,28 +62,12 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
// Controller information
const controllerUrl = '';
const auth = new BasicAuth('', '');
const connection = await azdata.connection.openConnectionDialog(['MSSQL']);
const connectionProfile: azdata.IConnectionProfile = {
serverName: connection.options['serverName'],
databaseName: connection.options['databaseName'],
authenticationType: connection.options['authenticationType'],
providerName: 'MSSQL',
connectionName: '',
userName: connection.options['user'],
password: connection.options['password'],
savePassword: false,
groupFullName: undefined,
saveProfile: true,
id: connection.connectionId,
groupId: undefined,
options: connection.options
};
const instanceNamespace = '';
const instanceName = '';
try {
const controllerModel = new ControllerModel(controllerUrl, auth);
const miaaModel = new MiaaModel(connectionProfile, controllerUrl, auth, instanceNamespace, instanceName);
const miaaModel = new MiaaModel(controllerUrl, auth, instanceNamespace, instanceName);
const miaaDashboard = new MiaaDashboard(controllerModel, miaaModel);
await Promise.all([
@@ -85,7 +91,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
try {
const controllerModel = new ControllerModel(controllerUrl, auth);
const postgresModel = new PostgresModel(controllerUrl, auth, dbNamespace, dbName);
const postgresDashboard = new PostgresDashboard(loc.postgresDashboard, context, controllerModel, postgresModel);
const postgresDashboard = new PostgresDashboard(context, controllerModel, postgresModel);
await Promise.all([
postgresDashboard.showDashboard(),

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import { getErrorMessage } from './common/utils';
const localize = nls.loadMessageBundle();
export const arcControllerDashboard = localize('arc.controllerDashboard', "Azure Arc Controller Dashboard (Preview)");
@@ -64,6 +65,16 @@ export const clickTheNewSupportRequestButton = localize('arc.clickTheNewSupportR
export const running = localize('arc.running', "Running");
export const connected = localize('arc.connected', "Connected");
export const disconnected = localize('arc.disconnected', "Disconnected");
export const loading = localize('arc.loading', "Loading...");
// Database States - see https://docs.microsoft.com/sql/relational-databases/databases/database-states
export const online = localize('arc.online', "Online");
export const offline = localize('arc.offline', "Offline");
export const restoring = localize('arc.restoring', "Restoring");
export const recovering = localize('arc.recovering', "Recovering");
export const recoveryPending = localize('arc.recoveringPending', "Recovery Pending");
export const suspect = localize('arc.suspect', "Suspect");
export const emergecy = localize('arc.emergecy', "Emergecy");
// Postgres constants
export const coordinatorEndpoint = localize('arc.coordinatorEndpoint', "Coordinator endpoint");
@@ -87,15 +98,16 @@ export const storagePerNode = localize('arc.storagePerNode', "storage per node")
export const arcResources = localize('arc.arcResources', "Azure Arc Resources");
export function databaseCreated(name: string): string { return localize('arc.databaseCreated', "Database {0} created", name); }
export function databaseCreationFailed(name: string, error: any): string { return localize('arc.databaseCreationFailed', "Failed to create database {0}. {1}", name, (error instanceof Error ? error.message : error)); }
export function databaseCreationFailed(name: string, error: any): string { return localize('arc.databaseCreationFailed', "Failed to create database {0}. {1}", name, getErrorMessage(error)); }
export function passwordReset(name: string): string { return localize('arc.passwordReset', "Password reset for service {0}", name); }
export function passwordResetFailed(name: string, error: any): string { return localize('arc.passwordResetFailed', "Failed to reset password for service {0}. {1}", name, (error instanceof Error ? error.message : error)); }
export function passwordResetFailed(name: string, error: any): string { return localize('arc.passwordResetFailed', "Failed to reset password for service {0}. {1}", name, getErrorMessage(error)); }
export function resourceDeleted(name: string): string { return localize('arc.resourceDeleted', "Resource '{0}' deleted", name); }
export function resourceDeletionFailed(name: string, error: any): string { return localize('arc.resourceDeletionFailed', "Failed to delete resource {0}. {1}", name, (error instanceof Error ? error.message : error)); }
export function resourceDeletionFailed(name: string, error: any): string { return localize('arc.resourceDeletionFailed', "Failed to delete resource {0}. {1}", name, getErrorMessage(error)); }
export function couldNotFindAzureResource(name: string): string { return localize('arc.couldNotFindAzureResource', "Could not find Azure resource for {0}", name); }
export function copiedToClipboard(name: string): string { return localize('arc.copiedToClipboard', "{0} copied to clipboard", name); }
export function refreshFailed(error: any): string { return localize('arc.refreshFailed', "Refresh failed. {0}", (error instanceof Error ? error.message : error)); }
export function failedToManagePostgres(name: string, error: any): string { return localize('arc.failedToManagePostgres', "Failed to manage Postgres {0}. {1}", name, (error instanceof Error ? error.message : error)); }
export function refreshFailed(error: any): string { return localize('arc.refreshFailed', "Refresh failed. {0}", getErrorMessage(error)); }
export function failedToManagePostgres(name: string, error: any): string { return localize('arc.failedToManagePostgres', "Failed to manage Postgres {0}. {1}", name, getErrorMessage(error)); }
export function openDashboardFailed(error: any): string { return localize('arc.openDashboardFailed', "Error opening dashboard. {0}", getErrorMessage(error)); }
export function clickTheTroubleshootButton(resourceType: string): string { return localize('arc.clickTheTroubleshootButton', "Click the troubleshoot button to open the Azure Arc {0} troubleshooting notebook.", resourceType); }
export function numVCores(vCores: string): string {
const numCores = +vCores;

View File

@@ -6,7 +6,7 @@
import * as vscode from 'vscode';
import { Authentication } from '../controller/auth';
import { EndpointsRouterApi, EndpointModel, RegistrationRouterApi, RegistrationResponse, TokenRouterApi, SqlInstanceRouterApi } from '../controller/generated/v1/api';
import { parseEndpoint } from '../common/utils';
import { parseEndpoint, parseInstanceName } from '../common/utils';
import { ResourceType } from '../constants';
export interface Registration extends RegistrationResponse {
@@ -31,7 +31,7 @@ export class ControllerModel {
public endpointsLastUpdated?: Date;
public registrationsLastUpdated?: Date;
constructor(controllerUrl: string, auth: Authentication) {
constructor(public readonly controllerUrl: string, public readonly auth: Authentication) {
this._endpointsRouter = new EndpointsRouterApi(controllerUrl);
this._endpointsRouter.setDefaultAuthentication(auth);
@@ -84,16 +84,7 @@ export class ControllerModel {
public getRegistration(type: string, namespace: string, name: string): Registration | undefined {
return this._registrations.find(r => {
// Resources deployed outside the controller's namespace are named in the format 'namespace_name'
let instanceName = r.instanceName!;
const parts: string[] = instanceName.split('_');
if (parts.length === 2) {
instanceName = parts[1];
}
else if (parts.length > 2) {
throw new Error(`Cannot parse resource '${instanceName}'. Acceptable formats are 'namespace_name' or 'name'.`);
}
return r.instanceType === type && r.instanceNamespace === namespace && instanceName === name;
return r.instanceType === type && r.instanceNamespace === namespace && parseInstanceName(r.instanceName) === name;
});
}

View File

@@ -16,6 +16,8 @@ export class MiaaModel {
private _sqlInstanceRouter: SqlInstanceRouterApi;
private _status: HybridSqlNsNameGetResponse | undefined;
private _databases: DatabaseModel[] = [];
private _connectionProfile: azdata.IConnectionProfile | undefined = undefined;
private readonly _onPasswordUpdated = new vscode.EventEmitter<string>();
private readonly _onStatusUpdated = new vscode.EventEmitter<HybridSqlNsNameGetResponse>();
private readonly _onDatabasesUpdated = new vscode.EventEmitter<DatabaseModel[]>();
@@ -24,7 +26,7 @@ export class MiaaModel {
public onDatabasesUpdated = this._onDatabasesUpdated.event;
public passwordLastUpdated?: Date;
constructor(public connectionProfile: azdata.IConnectionProfile, controllerUrl: string, auth: Authentication, private _namespace: string, private _name: string) {
constructor(controllerUrl: string, auth: Authentication, private _namespace: string, private _name: string) {
this._sqlInstanceRouter = new SqlInstanceRouterApi(controllerUrl);
this._sqlInstanceRouter.setDefaultAuthentication(auth);
}
@@ -43,6 +45,13 @@ export class MiaaModel {
return this._namespace;
}
/**
* The username used to connect to this instance
*/
public get username(): string | undefined {
return this._connectionProfile?.userName;
}
/**
* The status of this instance
*/
@@ -67,17 +76,44 @@ export class MiaaModel {
this._status = response.body;
this._onStatusUpdated.fire(this._status);
});
const provider = azdata.dataprotocol.getProvider<azdata.MetadataProvider>(this.connectionProfile.providerName, azdata.DataProviderType.MetadataProvider);
const databasesRefresh = azdata.connection.getUriForConnection(this.connectionProfile.id).then(ownerUri => {
provider.getDatabases(ownerUri).then(databases => {
if (databases.length > 0 && typeof (databases[0]) === 'object') {
this._databases = (<azdata.DatabaseInfo[]>databases).map(db => { return { name: db.options['name'], status: db.options['state'] }; });
} else {
this._databases = (<string[]>databases).map(db => { return { name: db, status: '-' }; });
}
this._onDatabasesUpdated.fire(this._databases);
const promises: Thenable<any>[] = [instanceRefresh];
await this.getConnection();
if (this._connectionProfile) {
const provider = azdata.dataprotocol.getProvider<azdata.MetadataProvider>(this._connectionProfile.providerName, azdata.DataProviderType.MetadataProvider);
const databasesRefresh = azdata.connection.getUriForConnection(this._connectionProfile.id).then(ownerUri => {
provider.getDatabases(ownerUri).then(databases => {
if (databases.length > 0 && typeof (databases[0]) === 'object') {
this._databases = (<azdata.DatabaseInfo[]>databases).map(db => { return { name: db.options['name'], status: db.options['state'] }; });
} else {
this._databases = (<string[]>databases).map(db => { return { name: db, status: '-' }; });
}
this._onDatabasesUpdated.fire(this._databases);
});
});
});
await Promise.all([instanceRefresh, databasesRefresh]);
promises.push(databasesRefresh);
}
await Promise.all(promises);
}
private async getConnection(): Promise<void> {
if (this._connectionProfile) {
return;
}
const connection = await azdata.connection.openConnectionDialog(['MSSQL']);
this._connectionProfile = {
serverName: connection.options['serverName'],
databaseName: connection.options['databaseName'],
authenticationType: connection.options['authenticationType'],
providerName: 'MSSQL',
connectionName: '',
userName: connection.options['user'],
password: connection.options['password'],
savePassword: false,
groupFullName: undefined,
saveProfile: true,
id: connection.connectionId,
groupId: undefined,
options: connection.options
};
}
}

View File

@@ -78,7 +78,7 @@ export class MiaaConnectionStringsPage extends DashboardPage {
const ip = this._instanceRegistration.externalIp;
const port = this._instanceRegistration.externalPort;
const username = this._miaaModel.connectionProfile.userName;
const username = this._miaaModel.username;
const pairs: KeyValue[] = [
new InputKeyValue('ADO.NET', `Server=tcp:${ip},${port};Persist Security Info=False;User ID=${username};Password={your_password_here};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;`),

View File

@@ -9,7 +9,7 @@ import * as loc from '../../../localizedConstants';
import { DashboardPage } from '../../components/dashboardPage';
import { IconPathHelper, cssStyles, ResourceType } from '../../../constants';
import { ControllerModel, Registration } from '../../../models/controllerModel';
import { getAzurecoreApi, promptForResourceDeletion, getErrorText } from '../../../common/utils';
import { getAzurecoreApi, promptForResourceDeletion, getDatabaseStateDisplayText } from '../../../common/utils';
import { MiaaModel, DatabaseModel } from '../../../models/miaaModel';
import { HybridSqlNsNameGetResponse } from '../../../controller/generated/v1/model/hybridSqlNsNameGetResponse';
import { EndpointModel } from '../../../controller/generated/v1/api';
@@ -39,7 +39,7 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
constructor(modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
super(modelView);
this._instanceProperties.miaaAdmin = this._miaaModel.connectionProfile.userName;
this._instanceProperties.miaaAdmin = this._miaaModel.username || this._instanceProperties.miaaAdmin;
this._controllerModel.onRegistrationsUpdated((_: Registration[]) => {
this.eventuallyRunOnInitialized(() => {
this.handleRegistrationsUpdated().catch(e => console.log(e));
@@ -184,7 +184,7 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
vscode.window.showInformationMessage(loc.resourceDeleted(this._miaaModel.name));
}
} catch (error) {
vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._miaaModel.name, getErrorText(error)));
vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._miaaModel.name, error));
} finally {
deleteButton.enabled = true;
}
@@ -253,7 +253,10 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
}
private handleDatabasesUpdated(databases: DatabaseModel[]): void {
this._databasesTable.data = databases.map(d => [d.name, d.status]);
// If we were able to get the databases it means we have a good connection so update the username too
this._instanceProperties.miaaAdmin = this._miaaModel.username || this._instanceProperties.miaaAdmin;
this.refreshDisplayedProperties();
this._databasesTable.data = databases.map(d => [d.name, getDatabaseStateDisplayText(d.status)]);
this._databasesTableLoading.loading = false;
}

View File

@@ -19,8 +19,8 @@ import { PostgresDiagnoseAndSolveProblemsPage } from './postgresDiagnoseAndSolve
import { PostgresSupportRequestPage } from './postgresSupportRequestPage';
export class PostgresDashboard extends Dashboard {
constructor(title: string, private _context: vscode.ExtensionContext, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(title);
constructor(private _context: vscode.ExtensionContext, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) {
super(loc.postgresDashboard);
}
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {

View File

@@ -11,7 +11,7 @@ import { DuskyObjectModelsDatabase, DuskyObjectModelsDatabaseServiceArcPayload,
import { DashboardPage } from '../../components/dashboardPage';
import { ControllerModel } from '../../../models/controllerModel';
import { PostgresModel, PodRole } from '../../../models/postgresModel';
import { promptForResourceDeletion, getErrorText } from '../../../common/utils';
import { promptForResourceDeletion } from '../../../common/utils';
export class PostgresOverviewPage extends DashboardPage {
private propertiesLoading?: azdata.LoadingComponent;
@@ -226,7 +226,7 @@ export class PostgresOverviewPage extends DashboardPage {
vscode.window.showInformationMessage(loc.resourceDeleted(this._postgresModel.fullName));
}
} catch (error) {
vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._postgresModel.fullName, getErrorText(error)));
vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._postgresModel.fullName, error));
} finally {
deleteButton.enabled = true;
}

View File

@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ControllerTreeNode } from './controllerTreeNode';
import { TreeNode } from './treeNode';
import { LoadingControllerNode as LoadingTreeNode } from './loadingTreeNode';
import { ControllerModel } from '../../models/controllerModel';
/**
* The TreeDataProvider for the Azure Arc view, which displays a list of registered
* controllers and the resources under them.
*/
export class AzureArcTreeDataProvider implements vscode.TreeDataProvider<TreeNode> {
private _onDidChangeTreeData: vscode.EventEmitter<TreeNode | undefined> = new vscode.EventEmitter<TreeNode | undefined>();
readonly onDidChangeTreeData: vscode.Event<TreeNode | undefined> = this._onDidChangeTreeData.event;
private _loading: boolean = true;
private _loadingNode = new LoadingTreeNode();
private _controllerNodes: ControllerTreeNode[] = [];
constructor(private _context: vscode.ExtensionContext) {
// TODO:
setTimeout(() => {
this._loading = false;
this._onDidChangeTreeData.fire(undefined);
}, 5000);
}
public async getChildren(element?: TreeNode): Promise<TreeNode[]> {
if (this._loading) {
return [this._loadingNode];
}
if (element) {
return element.getChildren();
} else {
return this._controllerNodes;
}
}
public getTreeItem(element: TreeNode): TreeNode | Thenable<TreeNode> {
return element;
}
public addController(model: ControllerModel): void {
this._controllerNodes.push(new ControllerTreeNode(model, this._context));
this._onDidChangeTreeData.fire(undefined);
}
public removeController(controllerNode: ControllerTreeNode): void {
this._controllerNodes = this._controllerNodes.filter(node => node !== controllerNode);
this._onDidChangeTreeData.fire(undefined);
}
}

View File

@@ -0,0 +1,56 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { TreeNode } from './treeNode';
import { MiaaTreeNode } from './miaaTreeNode';
import { ResourceType } from '../../constants';
import { PostgresTreeNode } from './postgresTreeNode';
import { ControllerModel, Registration } from '../../models/controllerModel';
import { ControllerDashboard } from '../dashboards/controller/controllerDashboard';
import { PostgresModel } from '../../models/postgresModel';
import { parseInstanceName } from '../../common/utils';
import { MiaaModel } from '../../models/miaaModel';
/**
* The TreeNode for displaying an Azure Arc Controller
*/
export class ControllerTreeNode extends TreeNode {
private _children: TreeNode[] = [];
constructor(private _model: ControllerModel, private _context: vscode.ExtensionContext) {
super(_model.controllerUrl, vscode.TreeItemCollapsibleState.Collapsed, ResourceType.dataControllers);
_model.onRegistrationsUpdated(registrations => this.refreshChildren(registrations));
_model.refresh().catch(err => console.log(`Error refreshing Arc Controller model for tree node : ${err}`));
}
public async getChildren(): Promise<TreeNode[]> {
return this._children;
}
public async openDashboard(): Promise<void> {
const controllerDashboard = new ControllerDashboard(this._model);
await controllerDashboard.showDashboard();
}
private refreshChildren(registrations: Registration[]): void {
this._children = <TreeNode[]>registrations.map(registration => {
if (!registration.instanceNamespace || !registration.instanceName) {
console.warn('Registration is missing required namespace and name values, skipping');
return undefined;
}
switch (registration.instanceType) {
case ResourceType.postgresInstances:
const postgresModel = new PostgresModel(this._model.controllerUrl, this._model.auth, registration.instanceNamespace, parseInstanceName(registration.instanceName));
return new PostgresTreeNode(postgresModel, this._model, this._context);
case ResourceType.sqlManagedInstances:
const miaaModel = new MiaaModel(this._model.controllerUrl, this._model.auth, registration.instanceNamespace, parseInstanceName(registration.instanceName));
return new MiaaTreeNode(miaaModel, this._model);
}
return undefined;
}).filter(item => item); // filter out invalid nodes (controllers or ones without required properties)
}
}

View File

@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as loc from '../../localizedConstants';
import { TreeNode } from './treeNode';
/**
* A placeholder TreeNode to display while we're loading the initial set of stored nodes
*/
export class LoadingControllerNode extends TreeNode {
constructor() {
super(loc.loading, vscode.TreeItemCollapsibleState.None, 'loading');
}
}

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ResourceType } from '../../constants';
import { TreeNode } from './treeNode';
import { MiaaModel } from '../../models/miaaModel';
import { ControllerModel } from '../../models/controllerModel';
import { MiaaDashboard } from '../dashboards/miaa/miaaDashboard';
/**
* The TreeNode for displaying a SQL Managed Instance on Azure Arc
*/
export class MiaaTreeNode extends TreeNode {
constructor(private _model: MiaaModel, private _controllerModel: ControllerModel) {
super(_model.name, vscode.TreeItemCollapsibleState.None, ResourceType.sqlManagedInstances);
}
public async openDashboard(): Promise<void> {
const miaaDashboard = new MiaaDashboard(this._controllerModel, this._model);
await Promise.all([
miaaDashboard.showDashboard(),
this._model.refresh()]);
}
}

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ResourceType } from '../../constants';
import { TreeNode } from './treeNode';
import { PostgresModel } from '../../models/postgresModel';
import { ControllerModel } from '../../models/controllerModel';
import { PostgresDashboard } from '../dashboards/postgres/postgresDashboard';
/**
* The TreeNode for displaying an Postgres Server group
*/
export class PostgresTreeNode extends TreeNode {
constructor(private _model: PostgresModel, private _controllerModel: ControllerModel, private _context: vscode.ExtensionContext) {
super(_model.name, vscode.TreeItemCollapsibleState.None, ResourceType.postgresInstances);
}
public async openDashboard(): Promise<void> {
const postgresDashboard = new PostgresDashboard(this._context, this._controllerModel, this._model);
await Promise.all([
postgresDashboard.showDashboard(),
this._model.refresh()]);
}
}

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { getResourceTypeIcon } from '../../common/utils';
/**
* The base class for a TreeNode to be displayed in the TreeView
*/
export abstract class TreeNode extends vscode.TreeItem {
constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState, private resourceType?: string) {
super(label, collapsibleState);
}
public async getChildren(): Promise<TreeNode[]> {
return [];
}
public async openDashboard(): Promise<void> { }
iconPath = getResourceTypeIcon(this.resourceType);
contextValue = this.resourceType;
}