diff --git a/extensions/arc/images/collapse-down-inverse.svg b/extensions/arc/images/collapse-down-inverse.svg
deleted file mode 100644
index 81848c6598..0000000000
--- a/extensions/arc/images/collapse-down-inverse.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/extensions/arc/images/collapse-down.svg b/extensions/arc/images/collapse-down.svg
deleted file mode 100644
index 122ab8abfb..0000000000
--- a/extensions/arc/images/collapse-down.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/extensions/arc/images/collapse-up-inverse.svg b/extensions/arc/images/collapse-up-inverse.svg
deleted file mode 100644
index a207d0f145..0000000000
--- a/extensions/arc/images/collapse-up-inverse.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/extensions/arc/images/collapse-up.svg b/extensions/arc/images/collapse-up.svg
deleted file mode 100644
index 4c7c97e181..0000000000
--- a/extensions/arc/images/collapse-up.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/extensions/arc/images/data_controller.svg b/extensions/arc/images/data_controller.svg
new file mode 100644
index 0000000000..5e2274e46b
--- /dev/null
+++ b/extensions/arc/images/data_controller.svg
@@ -0,0 +1,23 @@
+
diff --git a/extensions/arc/package.json b/extensions/arc/package.json
index 89c6b55d27..f49bf2846b 100644
--- a/extensions/arc/package.json
+++ b/extensions/arc/package.json
@@ -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"
}
]
},
diff --git a/extensions/arc/package.nls.json b/extensions/arc/package.nls.json
index 317a1f4305..3158a8c03a 100644
--- a/extensions/arc/package.nls.json
+++ b/extensions/arc/package.nls.json
@@ -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"
}
diff --git a/extensions/arc/src/common/utils.ts b/extensions/arc/src/common/utils.ts
index 4d52d5b219..19954753a5 100644
--- a/extensions/arc/src/common/utils.ts
+++ b/extensions/arc/src/common/utils.ts
@@ -46,16 +46,26 @@ export async function getAzurecoreApi(): Promise {
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;
+}
diff --git a/extensions/arc/src/constants.ts b/extensions/arc/src/constants.ts
index f20c9d4b1d..901aa348ea 100644
--- a/extensions/arc/src/constants.ts
+++ b/extensions/arc/src/constants.ts
@@ -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'),
+ };
}
}
diff --git a/extensions/arc/src/extension.ts b/extensions/arc/src/extension.ts
index 58b4400ad9..24356eff55 100644
--- a/extensions/arc/src/extension.ts
+++ b/extensions/arc/src/extension.ts
@@ -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 {
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
// 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
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(),
diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts
index 752939bec6..071a1e94e2 100644
--- a/extensions/arc/src/localizedConstants.ts
+++ b/extensions/arc/src/localizedConstants.ts
@@ -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;
diff --git a/extensions/arc/src/models/controllerModel.ts b/extensions/arc/src/models/controllerModel.ts
index 2edf5bd088..1d72e6e27e 100644
--- a/extensions/arc/src/models/controllerModel.ts
+++ b/extensions/arc/src/models/controllerModel.ts
@@ -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;
});
}
diff --git a/extensions/arc/src/models/miaaModel.ts b/extensions/arc/src/models/miaaModel.ts
index 5b9e8906b0..19b2723bde 100644
--- a/extensions/arc/src/models/miaaModel.ts
+++ b/extensions/arc/src/models/miaaModel.ts
@@ -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();
private readonly _onStatusUpdated = new vscode.EventEmitter();
private readonly _onDatabasesUpdated = new vscode.EventEmitter();
@@ -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(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 = (databases).map(db => { return { name: db.options['name'], status: db.options['state'] }; });
- } else {
- this._databases = (databases).map(db => { return { name: db, status: '-' }; });
- }
- this._onDatabasesUpdated.fire(this._databases);
+ const promises: Thenable[] = [instanceRefresh];
+ await this.getConnection();
+ if (this._connectionProfile) {
+ const provider = azdata.dataprotocol.getProvider(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 = (databases).map(db => { return { name: db.options['name'], status: db.options['state'] }; });
+ } else {
+ this._databases = (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 {
+ 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
+ };
}
}
diff --git a/extensions/arc/src/ui/dashboards/miaa/miaaConnectionStringsPage.ts b/extensions/arc/src/ui/dashboards/miaa/miaaConnectionStringsPage.ts
index 38b0db9e3b..389658e530 100644
--- a/extensions/arc/src/ui/dashboards/miaa/miaaConnectionStringsPage.ts
+++ b/extensions/arc/src/ui/dashboards/miaa/miaaConnectionStringsPage.ts
@@ -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;`),
diff --git a/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts b/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts
index 1ca1c473e7..502dffd6ec 100644
--- a/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts
+++ b/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts
@@ -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;
}
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts
index 6adcda34e2..0bb2b326c1 100644
--- a/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts
+++ b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts
@@ -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)[]> {
diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts
index c15ae91889..5ef316c396 100644
--- a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts
+++ b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts
@@ -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;
}
diff --git a/extensions/arc/src/ui/tree/controllerTreeDataProvider.ts b/extensions/arc/src/ui/tree/controllerTreeDataProvider.ts
new file mode 100644
index 0000000000..77ea51cf49
--- /dev/null
+++ b/extensions/arc/src/ui/tree/controllerTreeDataProvider.ts
@@ -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 {
+
+ private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter();
+ readonly onDidChangeTreeData: vscode.Event = 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 {
+ if (this._loading) {
+ return [this._loadingNode];
+ }
+
+ if (element) {
+ return element.getChildren();
+ } else {
+ return this._controllerNodes;
+ }
+ }
+
+ public getTreeItem(element: TreeNode): TreeNode | Thenable {
+ 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);
+ }
+}
diff --git a/extensions/arc/src/ui/tree/controllerTreeNode.ts b/extensions/arc/src/ui/tree/controllerTreeNode.ts
new file mode 100644
index 0000000000..69d2a724da
--- /dev/null
+++ b/extensions/arc/src/ui/tree/controllerTreeNode.ts
@@ -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 {
+ return this._children;
+ }
+
+ public async openDashboard(): Promise {
+ const controllerDashboard = new ControllerDashboard(this._model);
+ await controllerDashboard.showDashboard();
+ }
+
+ private refreshChildren(registrations: Registration[]): void {
+ this._children = 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)
+ }
+}
diff --git a/extensions/arc/src/ui/tree/loadingTreeNode.ts b/extensions/arc/src/ui/tree/loadingTreeNode.ts
new file mode 100644
index 0000000000..4a4628a190
--- /dev/null
+++ b/extensions/arc/src/ui/tree/loadingTreeNode.ts
@@ -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');
+ }
+}
diff --git a/extensions/arc/src/ui/tree/miaaTreeNode.ts b/extensions/arc/src/ui/tree/miaaTreeNode.ts
new file mode 100644
index 0000000000..bcb7ef1809
--- /dev/null
+++ b/extensions/arc/src/ui/tree/miaaTreeNode.ts
@@ -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 {
+ const miaaDashboard = new MiaaDashboard(this._controllerModel, this._model);
+ await Promise.all([
+ miaaDashboard.showDashboard(),
+ this._model.refresh()]);
+ }
+}
diff --git a/extensions/arc/src/ui/tree/postgresTreeNode.ts b/extensions/arc/src/ui/tree/postgresTreeNode.ts
new file mode 100644
index 0000000000..c3409a917c
--- /dev/null
+++ b/extensions/arc/src/ui/tree/postgresTreeNode.ts
@@ -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 {
+ const postgresDashboard = new PostgresDashboard(this._context, this._controllerModel, this._model);
+ await Promise.all([
+ postgresDashboard.showDashboard(),
+ this._model.refresh()]);
+ }
+}
diff --git a/extensions/arc/src/ui/tree/treeNode.ts b/extensions/arc/src/ui/tree/treeNode.ts
new file mode 100644
index 0000000000..252e62ed63
--- /dev/null
+++ b/extensions/arc/src/ui/tree/treeNode.ts
@@ -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 {
+ return [];
+ }
+
+ public async openDashboard(): Promise { }
+
+ iconPath = getResourceTypeIcon(this.resourceType);
+ contextValue = this.resourceType;
+}