mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-13 17:22:15 -05:00
Add persistence and connect dialog to Arc view (#11014)
* Add controller persistence and info prompting * more stuff * clean up * Add arc tests to scripts
This commit is contained in:
20
extensions/arc/coverConfig.json
Normal file
20
extensions/arc/coverConfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"relativeSourcePath": "..",
|
||||||
|
"relativeCoverageDir": "../../coverage",
|
||||||
|
"ignorePatterns": [
|
||||||
|
"**/generated/**",
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/test/**"
|
||||||
|
],
|
||||||
|
"reports": [
|
||||||
|
"cobertura",
|
||||||
|
"lcov",
|
||||||
|
"json"
|
||||||
|
],
|
||||||
|
"verbose": false,
|
||||||
|
"remapOptions": {
|
||||||
|
"basePath": "..",
|
||||||
|
"useAbsolutePaths": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,13 +49,22 @@
|
|||||||
"title": "%arc.openDashboard%"
|
"title": "%arc.openDashboard%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "arc.addController",
|
"command": "arc.createController",
|
||||||
"title": "%command.addController.title%",
|
"title": "%command.createController.title%",
|
||||||
"icon": "$(add)"
|
"icon": "$(add)"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "arc.connectToController",
|
||||||
|
"title": "%command.connectToController.title%",
|
||||||
|
"icon": "$(debug-disconnect)"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "arc.removeController",
|
"command": "arc.removeController",
|
||||||
"title": "%command.removeController.title%"
|
"title": "%command.removeController.title%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "arc.refresh",
|
||||||
|
"title": "%command.refresh.title%"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"menus": {
|
"menus": {
|
||||||
@@ -79,13 +88,22 @@
|
|||||||
{
|
{
|
||||||
"command": "arc.removeController",
|
"command": "arc.removeController",
|
||||||
"when": "false"
|
"when": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "arc.refresh",
|
||||||
|
"when": "false"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"view/title": [
|
"view/title": [
|
||||||
{
|
{
|
||||||
"command": "arc.addController",
|
"command": "arc.createController",
|
||||||
"when": "view == azureArc",
|
"when": "view == azureArc",
|
||||||
"group": "navigation"
|
"group": "navigation@1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "arc.connectToController",
|
||||||
|
"when": "view == azureArc",
|
||||||
|
"group": "navigation@2"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"view/item/context": [
|
"view/item/context": [
|
||||||
@@ -95,9 +113,14 @@
|
|||||||
"group": "navigation@1"
|
"group": "navigation@1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "arc.removeController",
|
"command": "arc.refresh",
|
||||||
"when": "view == azureArc && viewItem == dataControllers",
|
"when": "view == azureArc && viewItem == dataControllers",
|
||||||
"group": "navigation@2"
|
"group": "navigation@2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "arc.removeController",
|
||||||
|
"when": "view == azureArc && viewItem == dataControllers",
|
||||||
|
"group": "navigation@3"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -125,6 +148,7 @@
|
|||||||
"mocha-junit-reporter": "^1.17.0",
|
"mocha-junit-reporter": "^1.17.0",
|
||||||
"mocha-multi-reporters": "^1.1.7",
|
"mocha-multi-reporters": "^1.1.7",
|
||||||
"should": "^13.2.3",
|
"should": "^13.2.3",
|
||||||
|
"typemoq": "2.1.0",
|
||||||
"vscodetestcover": "^1.0.9"
|
"vscodetestcover": "^1.0.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
"arc.managePostgres": "Manage Postgres",
|
"arc.managePostgres": "Manage Postgres",
|
||||||
"arc.manageArcController": "Manage Arc Controller",
|
"arc.manageArcController": "Manage Arc Controller",
|
||||||
"arc.view.title" : "Azure Arc Controllers",
|
"arc.view.title" : "Azure Arc Controllers",
|
||||||
"command.addController.title": "Connect to Controller",
|
"command.createController.title" : "Create New Controller",
|
||||||
|
"command.connectToController.title": "Connect to Existing Controller",
|
||||||
"command.removeController.title": "Remove Controller",
|
"command.removeController.title": "Remove Controller",
|
||||||
|
"command.refresh.title": "Refresh",
|
||||||
"arc.openDashboard": "Manage"
|
"arc.openDashboard": "Manage"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export const refreshActionId = 'arc.refresh';
|
||||||
|
|
||||||
export interface IconPath {
|
export interface IconPath {
|
||||||
dark: string;
|
dark: string;
|
||||||
light: string;
|
light: string;
|
||||||
|
|||||||
@@ -5,17 +5,11 @@
|
|||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as loc from './localizedConstants';
|
import * as loc from './localizedConstants';
|
||||||
import { IconPathHelper } from './constants';
|
import { IconPathHelper, refreshActionId } from './constants';
|
||||||
import { BasicAuth } from './controller/auth';
|
import { AzureArcTreeDataProvider } from './ui/tree/azureArcTreeDataProvider';
|
||||||
import { PostgresDashboard } from './ui/dashboards/postgres/postgresDashboard';
|
|
||||||
import { ControllerModel } from './models/controllerModel';
|
|
||||||
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 { ControllerTreeNode } from './ui/tree/controllerTreeNode';
|
||||||
import { TreeNode } from './ui/tree/treeNode';
|
import { TreeNode } from './ui/tree/treeNode';
|
||||||
|
import { ConnectToControllerDialog } from './ui/dialogs/connectControllerDialog';
|
||||||
|
|
||||||
export async function activate(context: vscode.ExtensionContext): Promise<void> {
|
export async function activate(context: vscode.ExtensionContext): Promise<void> {
|
||||||
IconPathHelper.setExtensionContext(context);
|
IconPathHelper.setExtensionContext(context);
|
||||||
@@ -23,85 +17,30 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
|
|||||||
const treeDataProvider = new AzureArcTreeDataProvider(context);
|
const treeDataProvider = new AzureArcTreeDataProvider(context);
|
||||||
vscode.window.registerTreeDataProvider('azureArc', treeDataProvider);
|
vscode.window.registerTreeDataProvider('azureArc', treeDataProvider);
|
||||||
|
|
||||||
vscode.commands.registerCommand('arc.addController', () => {
|
vscode.commands.registerCommand('arc.createController', async () => {
|
||||||
// Controller information
|
await vscode.commands.executeCommand('azdata.resource.deploy');
|
||||||
const controllerUrl = '';
|
|
||||||
const auth = new BasicAuth('', '');
|
|
||||||
|
|
||||||
const controllerModel = new ControllerModel(controllerUrl, auth);
|
|
||||||
treeDataProvider.addController(controllerModel);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
vscode.commands.registerCommand('arc.removeController', (controllerNode: ControllerTreeNode) => {
|
vscode.commands.registerCommand('arc.connectToController', async () => {
|
||||||
treeDataProvider.removeController(controllerNode);
|
const dialog = new ConnectToControllerDialog(treeDataProvider);
|
||||||
|
dialog.showDialog();
|
||||||
|
const model = await dialog.waitForClose();
|
||||||
|
if (model) {
|
||||||
|
await treeDataProvider.addOrUpdateController(model.controllerModel, model.password);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
vscode.commands.registerCommand('arc.removeController', async (controllerNode: ControllerTreeNode) => {
|
||||||
|
await treeDataProvider.removeController(controllerNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
vscode.commands.registerCommand(refreshActionId, async (treeNode: TreeNode) => {
|
||||||
|
treeDataProvider.refreshNode(treeNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
vscode.commands.registerCommand('arc.openDashboard', async (treeNode: TreeNode) => {
|
vscode.commands.registerCommand('arc.openDashboard', async (treeNode: TreeNode) => {
|
||||||
await treeNode.openDashboard().catch(err => vscode.window.showErrorMessage(loc.openDashboardFailed(err)));
|
await treeNode.openDashboard().catch(err => vscode.window.showErrorMessage(loc.openDashboardFailed(err)));
|
||||||
});
|
});
|
||||||
|
|
||||||
vscode.commands.registerCommand('arc.manageArcController', async () => {
|
|
||||||
// Controller information
|
|
||||||
const controllerUrl = '';
|
|
||||||
const auth = new BasicAuth('', '');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const controllerModel = new ControllerModel(controllerUrl, auth);
|
|
||||||
const controllerDashboard = new ControllerDashboard(controllerModel);
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
controllerDashboard.showDashboard(),
|
|
||||||
controllerModel.refresh()
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
|
||||||
// vscode.window.showErrorMessage(loc.failedToManagePostgres(`${dbNamespace}.${dbName}`, error));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
vscode.commands.registerCommand('arc.manageMiaa', async () => {
|
|
||||||
// Controller information
|
|
||||||
const controllerUrl = '';
|
|
||||||
const auth = new BasicAuth('', '');
|
|
||||||
const instanceNamespace = '';
|
|
||||||
const instanceName = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const controllerModel = new ControllerModel(controllerUrl, auth);
|
|
||||||
const miaaModel = new MiaaModel(controllerUrl, auth, instanceNamespace, instanceName);
|
|
||||||
const miaaDashboard = new MiaaDashboard(controllerModel, miaaModel);
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
miaaDashboard.showDashboard(),
|
|
||||||
controllerModel.refresh()
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
|
||||||
// vscode.window.showErrorMessage(loc.failedToManagePostgres(`${dbNamespace}.${dbName}`, error));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
vscode.commands.registerCommand('arc.managePostgres', async () => {
|
|
||||||
// Controller information
|
|
||||||
const controllerUrl = '';
|
|
||||||
const auth = new BasicAuth('', '');
|
|
||||||
|
|
||||||
// Postgres information
|
|
||||||
const dbNamespace = '';
|
|
||||||
const dbName = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const controllerModel = new ControllerModel(controllerUrl, auth);
|
|
||||||
const postgresModel = new PostgresModel(controllerUrl, auth, dbNamespace, dbName);
|
|
||||||
const postgresDashboard = new PostgresDashboard(context, controllerModel, postgresModel);
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
postgresDashboard.showDashboard(),
|
|
||||||
controllerModel.refresh(),
|
|
||||||
postgresModel.refresh()
|
|
||||||
]);
|
|
||||||
} catch (error) {
|
|
||||||
vscode.window.showErrorMessage(loc.failedToManagePostgres(`${dbNamespace}.${dbName}`, error));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivate(): void {
|
export function deactivate(): void {
|
||||||
|
|||||||
@@ -66,6 +66,14 @@ export const running = localize('arc.running', "Running");
|
|||||||
export const connected = localize('arc.connected', "Connected");
|
export const connected = localize('arc.connected', "Connected");
|
||||||
export const disconnected = localize('arc.disconnected', "Disconnected");
|
export const disconnected = localize('arc.disconnected', "Disconnected");
|
||||||
export const loading = localize('arc.loading', "Loading...");
|
export const loading = localize('arc.loading', "Loading...");
|
||||||
|
export const refreshToEnterCredentials = localize('arc.refreshToEnterCredentials', "Refresh node to enter credentials");
|
||||||
|
export const connectToController = localize('arc.connectToController', "Connect to Existing Controller");
|
||||||
|
export const controllerUrl = localize('arc.controllerUrl', "Controller URL");
|
||||||
|
export const username = localize('arc.username', "Username");
|
||||||
|
export const password = localize('arc.password', "Password");
|
||||||
|
export const rememberPassword = localize('arc.rememberPassword', "Remember Password");
|
||||||
|
export const connect = localize('arc.connect', "Connect");
|
||||||
|
export const cancel = localize('arc.cancel', "Cancel");
|
||||||
|
|
||||||
// Database States - see https://docs.microsoft.com/sql/relational-databases/databases/database-states
|
// Database States - see https://docs.microsoft.com/sql/relational-databases/databases/database-states
|
||||||
export const online = localize('arc.online', "Online");
|
export const online = localize('arc.online', "Online");
|
||||||
@@ -98,16 +106,10 @@ export const storagePerNode = localize('arc.storagePerNode', "storage per node")
|
|||||||
export const arcResources = localize('arc.arcResources', "Azure Arc Resources");
|
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 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, getErrorMessage(error)); }
|
|
||||||
export function passwordReset(name: string): string { return localize('arc.passwordReset', "Password reset for service {0}", name); }
|
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, getErrorMessage(error)); }
|
|
||||||
export function resourceDeleted(name: string): string { return localize('arc.resourceDeleted', "Resource '{0}' deleted", name); }
|
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, getErrorMessage(error)); }
|
|
||||||
export function couldNotFindAzureResource(name: string): string { return localize('arc.couldNotFindAzureResource', "Could not find Azure resource for {0}", name); }
|
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 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}", 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 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 {
|
export function numVCores(vCores: string): string {
|
||||||
const numCores = +vCores;
|
const numCores = +vCores;
|
||||||
@@ -120,3 +122,11 @@ export function numVCores(vCores: string): string {
|
|||||||
export function couldNotFindRegistration(namespace: string, name: string) { return localize('arc.couldNotFindRegistration', "Could not find controller registration for {0} ({1})", name, namespace); }
|
export function couldNotFindRegistration(namespace: string, name: string) { return localize('arc.couldNotFindRegistration', "Could not find controller registration for {0} ({1})", name, namespace); }
|
||||||
export function resourceDeletionWarning(namespace: string, name: string): string { return localize('arc.resourceDeletionWarning', "Warning! Deleting a resource is permanent and cannot be undone. To delete the resource '{0}.{1}' type the name '{1}' below to proceed.", namespace, name); }
|
export function resourceDeletionWarning(namespace: string, name: string): string { return localize('arc.resourceDeletionWarning', "Warning! Deleting a resource is permanent and cannot be undone. To delete the resource '{0}.{1}' type the name '{1}' below to proceed.", namespace, name); }
|
||||||
export function invalidResourceDeletionName(name: string): string { return localize('arc.invalidResourceDeletionName', "The value '{0}' does not match the instance name. Try again or press escape to exit", name); }
|
export function invalidResourceDeletionName(name: string): string { return localize('arc.invalidResourceDeletionName', "The value '{0}' does not match the instance name. Try again or press escape to exit", name); }
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
export function refreshFailed(error: any): string { return localize('arc.refreshFailed', "Refresh failed. {0}", getErrorMessage(error)); }
|
||||||
|
export function openDashboardFailed(error: any): string { return localize('arc.openDashboardFailed', "Error opening dashboard. {0}", getErrorMessage(error)); }
|
||||||
|
export function resourceDeletionFailed(name: string, error: any): string { return localize('arc.resourceDeletionFailed', "Failed to delete resource {0}. {1}", name, getErrorMessage(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 databaseCreationFailed(name: string, error: any): string { return localize('arc.databaseCreationFailed', "Failed to create database {0}. {1}", name, getErrorMessage(error)); }
|
||||||
|
export function connectToControllerFailed(url: string, error: any): string { return localize('arc.connectToControllerFailed', "Could not connect to controller {0}. {1}", url, getErrorMessage(error)); }
|
||||||
|
|||||||
@@ -4,10 +4,18 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { Authentication } from '../controller/auth';
|
import { Authentication, BasicAuth } from '../controller/auth';
|
||||||
import { EndpointsRouterApi, EndpointModel, RegistrationRouterApi, RegistrationResponse, TokenRouterApi, SqlInstanceRouterApi } from '../controller/generated/v1/api';
|
import { EndpointsRouterApi, EndpointModel, RegistrationRouterApi, RegistrationResponse, TokenRouterApi, SqlInstanceRouterApi } from '../controller/generated/v1/api';
|
||||||
import { parseEndpoint, parseInstanceName } from '../common/utils';
|
import { parseEndpoint, parseInstanceName } from '../common/utils';
|
||||||
import { ResourceType } from '../constants';
|
import { ResourceType } from '../constants';
|
||||||
|
import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog';
|
||||||
|
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||||
|
|
||||||
|
export type ControllerInfo = {
|
||||||
|
url: string,
|
||||||
|
username: string,
|
||||||
|
rememberPassword: boolean
|
||||||
|
};
|
||||||
|
|
||||||
export interface Registration extends RegistrationResponse {
|
export interface Registration extends RegistrationResponse {
|
||||||
externalIp?: string;
|
externalIp?: string;
|
||||||
@@ -23,6 +31,7 @@ export class ControllerModel {
|
|||||||
private _namespace: string = '';
|
private _namespace: string = '';
|
||||||
private _registrations: Registration[] = [];
|
private _registrations: Registration[] = [];
|
||||||
private _controllerRegistration: Registration | undefined = undefined;
|
private _controllerRegistration: Registration | undefined = undefined;
|
||||||
|
private _auth: Authentication | undefined = undefined;
|
||||||
|
|
||||||
private readonly _onEndpointsUpdated = new vscode.EventEmitter<EndpointModel[]>();
|
private readonly _onEndpointsUpdated = new vscode.EventEmitter<EndpointModel[]>();
|
||||||
private readonly _onRegistrationsUpdated = new vscode.EventEmitter<Registration[]>();
|
private readonly _onRegistrationsUpdated = new vscode.EventEmitter<Registration[]>();
|
||||||
@@ -30,22 +39,42 @@ export class ControllerModel {
|
|||||||
public onRegistrationsUpdated = this._onRegistrationsUpdated.event;
|
public onRegistrationsUpdated = this._onRegistrationsUpdated.event;
|
||||||
public endpointsLastUpdated?: Date;
|
public endpointsLastUpdated?: Date;
|
||||||
public registrationsLastUpdated?: Date;
|
public registrationsLastUpdated?: Date;
|
||||||
|
public get auth(): Authentication | undefined {
|
||||||
|
return this._auth;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(public readonly controllerUrl: string, public readonly auth: Authentication) {
|
constructor(private _treeDataProvider: AzureArcTreeDataProvider, public info: ControllerInfo, password?: string) {
|
||||||
this._endpointsRouter = new EndpointsRouterApi(controllerUrl);
|
this._endpointsRouter = new EndpointsRouterApi(this.info.url);
|
||||||
this._endpointsRouter.setDefaultAuthentication(auth);
|
this._tokenRouter = new TokenRouterApi(this.info.url);
|
||||||
|
this._registrationRouter = new RegistrationRouterApi(this.info.url);
|
||||||
this._tokenRouter = new TokenRouterApi(controllerUrl);
|
this._sqlInstanceRouter = new SqlInstanceRouterApi(this.info.url);
|
||||||
this._tokenRouter.setDefaultAuthentication(auth);
|
if (password) {
|
||||||
|
this.setAuthentication(new BasicAuth(this.info.username, password));
|
||||||
this._registrationRouter = new RegistrationRouterApi(controllerUrl);
|
}
|
||||||
this._registrationRouter.setDefaultAuthentication(auth);
|
|
||||||
|
|
||||||
this._sqlInstanceRouter = new SqlInstanceRouterApi(controllerUrl);
|
|
||||||
this._sqlInstanceRouter.setDefaultAuthentication(auth);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async refresh(): Promise<void> {
|
public async refresh(): Promise<void> {
|
||||||
|
// We haven't gotten our password yet, fetch it now
|
||||||
|
if (!this._auth) {
|
||||||
|
let password = '';
|
||||||
|
if (this.info.rememberPassword) {
|
||||||
|
// It should be in the credentials store, get it from there
|
||||||
|
password = await this._treeDataProvider.getPassword(this.info);
|
||||||
|
}
|
||||||
|
if (password) {
|
||||||
|
this.setAuthentication(new BasicAuth(this.info.username, password));
|
||||||
|
} else {
|
||||||
|
// No password yet so prompt for it from the user
|
||||||
|
const dialog = new ConnectToControllerDialog(this._treeDataProvider);
|
||||||
|
dialog.showDialog(this.info);
|
||||||
|
const model = await dialog.waitForClose();
|
||||||
|
if (model) {
|
||||||
|
this._treeDataProvider.addOrUpdateController(model.controllerModel, model.password, false);
|
||||||
|
this.setAuthentication(new BasicAuth(this.info.username, model.password));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this._endpointsRouter.apiV1BdcEndpointsGet().then(response => {
|
this._endpointsRouter.apiV1BdcEndpointsGet().then(response => {
|
||||||
this._endpoints = response.body;
|
this._endpoints = response.body;
|
||||||
@@ -88,9 +117,31 @@ export class ControllerModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the specified MIAA resource from the controller
|
||||||
|
* @param namespace The namespace of the resource
|
||||||
|
* @param name The name of the resource
|
||||||
|
*/
|
||||||
public async miaaDelete(namespace: string, name: string): Promise<void> {
|
public async miaaDelete(namespace: string, name: string): Promise<void> {
|
||||||
await this._sqlInstanceRouter.apiV1HybridSqlNsNameDelete(namespace, name);
|
await this._sqlInstanceRouter.apiV1HybridSqlNsNameDelete(namespace, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether this model is for the same controller as another
|
||||||
|
* @param other The other instance to test
|
||||||
|
*/
|
||||||
|
public equals(other: ControllerModel): boolean {
|
||||||
|
return this.info.url === other.info.url &&
|
||||||
|
this.info.username === other.info.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setAuthentication(auth: Authentication): void {
|
||||||
|
this._auth = auth;
|
||||||
|
this._endpointsRouter.setDefaultAuthentication(auth);
|
||||||
|
this._tokenRouter.setDefaultAuthentication(auth);
|
||||||
|
this._registrationRouter.setDefaultAuthentication(auth);
|
||||||
|
this._sqlInstanceRouter.setDefaultAuthentication(auth);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import * as should from 'should';
|
import * as should from 'should';
|
||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { resourceTypeToDisplayName, parseEndpoint } from '../../common/utils';
|
import { resourceTypeToDisplayName, parseEndpoint, parseInstanceName } from '../../common/utils';
|
||||||
|
|
||||||
import * as loc from '../../localizedConstants';
|
import * as loc from '../../localizedConstants';
|
||||||
import { ResourceType } from '../../constants';
|
import { ResourceType } from '../../constants';
|
||||||
@@ -44,3 +44,20 @@ describe('parseEndpoint Method Tests', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('parseInstanceName Method Tests', () => {
|
||||||
|
it('Should parse valid instanceName with namespace correctly', function (): void {
|
||||||
|
should(parseInstanceName('mynamespace_myinstance')).equal('myinstance');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should parse valid instanceName without namespace correctly', function (): void {
|
||||||
|
should(parseInstanceName('myinstance')).equal('myinstance');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return empty string when undefined value passed in', function (): void {
|
||||||
|
should(parseInstanceName(undefined)).equal('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return empty string when empty string value passed in', function (): void {
|
||||||
|
should(parseInstanceName('')).equal('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
102
extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts
Normal file
102
extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 should from 'should';
|
||||||
|
import 'mocha';
|
||||||
|
import * as TypeMoq from 'typemoq';
|
||||||
|
import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider';
|
||||||
|
import { ControllerModel } from '../../../models/controllerModel';
|
||||||
|
import { ControllerTreeNode } from '../../../ui/tree/controllerTreeNode';
|
||||||
|
import { LoadingControllerNode } from '../../../ui/tree/loadingTreeNode';
|
||||||
|
|
||||||
|
describe('AzureArcTreeDataProvider tests', function (): void {
|
||||||
|
let treeDataProvider: AzureArcTreeDataProvider;
|
||||||
|
beforeEach(function (): void {
|
||||||
|
const mockExtensionContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
||||||
|
const mockGlobalState = TypeMoq.Mock.ofType<vscode.Memento>();
|
||||||
|
mockGlobalState.setup(x => x.update(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve());
|
||||||
|
mockExtensionContext.setup(x => x.globalState).returns(() => mockGlobalState.object);
|
||||||
|
//treeDataProviderMock = TypeMoq.Mock.ofType<AzureArcTreeDataProvider>();
|
||||||
|
treeDataProvider = new AzureArcTreeDataProvider(mockExtensionContext.object);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addOrUpdateController', function (): void {
|
||||||
|
it('Multiple Controllers are added correctly', async function (): Promise<void> {
|
||||||
|
treeDataProvider['_loading'] = false;
|
||||||
|
let children = await treeDataProvider.getChildren();
|
||||||
|
should(children.length).equal(0, 'There initially shouldn\'t be any children');
|
||||||
|
const controllerModelMock = TypeMoq.Mock.ofType<ControllerModel>();
|
||||||
|
await treeDataProvider.addOrUpdateController(controllerModelMock.object, '');
|
||||||
|
should(children.length).equal(1, 'Controller node should be added correctly');
|
||||||
|
|
||||||
|
// Add a couple more
|
||||||
|
const controllerModelMock2 = TypeMoq.Mock.ofType<ControllerModel>();
|
||||||
|
const controllerModelMock3 = TypeMoq.Mock.ofType<ControllerModel>();
|
||||||
|
await treeDataProvider.addOrUpdateController(controllerModelMock2.object, '');
|
||||||
|
await treeDataProvider.addOrUpdateController(controllerModelMock3.object, '');
|
||||||
|
should(children.length).equal(3, 'Additional Controller nodes should be added correctly');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Adding a Controller more than once doesn\'t create duplicates', async function (): Promise<void> {
|
||||||
|
treeDataProvider['_loading'] = false;
|
||||||
|
let children = await treeDataProvider.getChildren();
|
||||||
|
should(children.length).equal(0, 'There initially shouldn\'t be any children');
|
||||||
|
const controllerModel = new ControllerModel(treeDataProvider, { url: '127.0.0.1', username: 'sa', rememberPassword: true });
|
||||||
|
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||||
|
should(children.length).equal(1, 'Controller node should be added correctly');
|
||||||
|
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||||
|
should(children.length).equal(1, 'Shouldn\'t add duplicate controller node');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Updating an existing controller works as expected', async function (): Promise<void> {
|
||||||
|
treeDataProvider['_loading'] = false;
|
||||||
|
let children = await treeDataProvider.getChildren();
|
||||||
|
should(children.length).equal(0, 'There initially shouldn\'t be any children');
|
||||||
|
const controllerModel = new ControllerModel(treeDataProvider, { url: '127.0.0.1', username: 'sa', rememberPassword: true });
|
||||||
|
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||||
|
should(children.length).equal(1, 'Controller node should be added correctly');
|
||||||
|
should((<ControllerTreeNode>children[0]).model.info.rememberPassword).be.true('Info was not set correctly initially');
|
||||||
|
const controllerModel2 = new ControllerModel(treeDataProvider, { url: '127.0.0.1', username: 'sa', rememberPassword: false });
|
||||||
|
await treeDataProvider.addOrUpdateController(controllerModel2, '');
|
||||||
|
should(children.length).equal(1, 'Shouldn\'t add duplicate controller node');
|
||||||
|
should((<ControllerTreeNode>children[0]).model.info.rememberPassword).be.false('Info was not updated correctly');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getChildren', function (): void {
|
||||||
|
it('should return a loading node before loading stored controllers is completed', async function (): Promise<void> {
|
||||||
|
treeDataProvider['_loading'] = true;
|
||||||
|
let children = await treeDataProvider.getChildren();
|
||||||
|
should(children.length).equal(1, 'While loading we should return the loading node');
|
||||||
|
should(children[0] instanceof LoadingControllerNode).be.true('Node returned was not a LoadingControllerNode');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return no children after loading', async function (): Promise<void> {
|
||||||
|
treeDataProvider['_loading'] = false;
|
||||||
|
let children = await treeDataProvider.getChildren();
|
||||||
|
should(children.length).equal(0, 'After loading we should have 0 children');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeController', function (): void {
|
||||||
|
it('removing a controller should work as expected', async function (): Promise<void> {
|
||||||
|
treeDataProvider['_loading'] = false;
|
||||||
|
const controllerModel = new ControllerModel(treeDataProvider, { url: '127.0.0.1', username: 'sa', rememberPassword: true });
|
||||||
|
const controllerModel2 = new ControllerModel(treeDataProvider, { url: '127.0.0.2', username: 'cloudsa', rememberPassword: true });
|
||||||
|
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||||
|
await treeDataProvider.addOrUpdateController(controllerModel2, '');
|
||||||
|
const children = <ControllerTreeNode[]>(await treeDataProvider.getChildren());
|
||||||
|
await treeDataProvider.removeController(children[0]);
|
||||||
|
should((await treeDataProvider.getChildren()).length).equal(1, 'Node should have been removed');
|
||||||
|
await treeDataProvider.removeController(children[0]);
|
||||||
|
should((await treeDataProvider.getChildren()).length).equal(1, 'Removing same node again should do nothing');
|
||||||
|
await treeDataProvider.removeController(children[1]);
|
||||||
|
should((await treeDataProvider.getChildren()).length).equal(0, 'Removing other node should work');
|
||||||
|
await treeDataProvider.removeController(children[1]);
|
||||||
|
should((await treeDataProvider.getChildren()).length).equal(0, 'Removing other node again should do nothing');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
110
extensions/arc/src/ui/dialogs/connectControllerDialog.ts
Normal file
110
extensions/arc/src/ui/dialogs/connectControllerDialog.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as azdata from 'azdata';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as loc from '../../localizedConstants';
|
||||||
|
import { AzureArcTreeDataProvider } from '../tree/azureArcTreeDataProvider';
|
||||||
|
import { ControllerModel, ControllerInfo } from '../../models/controllerModel';
|
||||||
|
import { Deferred } from '../../common/promise';
|
||||||
|
|
||||||
|
export type ConnectToControllerDialogModel = { controllerModel: ControllerModel, password: string };
|
||||||
|
|
||||||
|
export class ConnectToControllerDialog {
|
||||||
|
private modelBuilder!: azdata.ModelBuilder;
|
||||||
|
|
||||||
|
private urlInputBox!: azdata.InputBoxComponent;
|
||||||
|
private usernameInputBox!: azdata.InputBoxComponent;
|
||||||
|
private passwordInputBox!: azdata.InputBoxComponent;
|
||||||
|
private rememberPwCheckBox!: azdata.CheckBoxComponent;
|
||||||
|
|
||||||
|
private _completionPromise = new Deferred<ConnectToControllerDialogModel | undefined>();
|
||||||
|
|
||||||
|
constructor(private _treeDataProvider: AzureArcTreeDataProvider) { }
|
||||||
|
|
||||||
|
public showDialog(controllerInfo?: ControllerInfo): void {
|
||||||
|
const dialog = azdata.window.createModelViewDialog(loc.connectToController);
|
||||||
|
dialog.cancelButton.onClick(() => this.handleCancel());
|
||||||
|
dialog.registerContent(async view => {
|
||||||
|
this.modelBuilder = view.modelBuilder;
|
||||||
|
|
||||||
|
this.urlInputBox = this.modelBuilder.inputBox()
|
||||||
|
.withProperties<azdata.InputBoxProperties>({
|
||||||
|
value: controllerInfo?.url,
|
||||||
|
// If we have a model then we're editing an existing connection so don't let them modify the URL
|
||||||
|
readOnly: !!controllerInfo
|
||||||
|
}).component();
|
||||||
|
this.usernameInputBox = this.modelBuilder.inputBox()
|
||||||
|
.withProperties<azdata.InputBoxProperties>({
|
||||||
|
value: controllerInfo?.username
|
||||||
|
}).component();
|
||||||
|
this.passwordInputBox = this.modelBuilder.inputBox()
|
||||||
|
.withProperties<azdata.InputBoxProperties>({
|
||||||
|
inputType: 'password',
|
||||||
|
})
|
||||||
|
.component();
|
||||||
|
this.rememberPwCheckBox = this.modelBuilder.checkBox()
|
||||||
|
.withProperties<azdata.CheckBoxProperties>({
|
||||||
|
label: loc.rememberPassword,
|
||||||
|
checked: controllerInfo?.rememberPassword
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
let formModel = this.modelBuilder.formContainer()
|
||||||
|
.withFormItems([{
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
component: this.urlInputBox,
|
||||||
|
title: loc.controllerUrl,
|
||||||
|
required: true
|
||||||
|
}, {
|
||||||
|
component: this.usernameInputBox,
|
||||||
|
title: loc.username,
|
||||||
|
required: true
|
||||||
|
}, {
|
||||||
|
component: this.passwordInputBox,
|
||||||
|
title: loc.password,
|
||||||
|
required: true
|
||||||
|
}, {
|
||||||
|
component: this.rememberPwCheckBox,
|
||||||
|
title: ''
|
||||||
|
}
|
||||||
|
],
|
||||||
|
title: ''
|
||||||
|
}]).withLayout({ width: '100%' }).component();
|
||||||
|
await view.initializeModel(formModel);
|
||||||
|
this.urlInputBox.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.registerCloseValidator(async () => await this.validate());
|
||||||
|
dialog.okButton.label = loc.connect;
|
||||||
|
dialog.cancelButton.label = loc.cancel;
|
||||||
|
azdata.window.openDialog(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async validate(): Promise<boolean> {
|
||||||
|
if (!this.urlInputBox.value || !this.usernameInputBox.value || !this.passwordInputBox.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const controllerInfo: ControllerInfo = { url: this.urlInputBox.value, username: this.usernameInputBox.value, rememberPassword: this.rememberPwCheckBox.checked ?? false };
|
||||||
|
const controllerModel = new ControllerModel(this._treeDataProvider, controllerInfo, this.passwordInputBox.value);
|
||||||
|
try {
|
||||||
|
// Validate that we can connect to the controller
|
||||||
|
await controllerModel.refresh();
|
||||||
|
} catch (err) {
|
||||||
|
vscode.window.showErrorMessage(loc.connectToControllerFailed(this.urlInputBox.value, err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this._completionPromise.resolve({ controllerModel: controllerModel, password: this.passwordInputBox.value });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCancel(): void {
|
||||||
|
this._completionPromise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public waitForClose(): Promise<ConnectToControllerDialogModel | undefined> {
|
||||||
|
return this._completionPromise.promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
113
extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts
Normal file
113
extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 { ControllerTreeNode } from './controllerTreeNode';
|
||||||
|
import { TreeNode } from './treeNode';
|
||||||
|
import { LoadingControllerNode as LoadingTreeNode } from './loadingTreeNode';
|
||||||
|
import { ControllerModel, ControllerInfo } from '../../models/controllerModel';
|
||||||
|
|
||||||
|
const mementoToken = 'arcControllers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 _credentialsProvider = azdata.credentials.getProvider('arcControllerPasswords');
|
||||||
|
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) {
|
||||||
|
this.loadSavedControllers().catch(err => console.log(`Error loading saved Arc controllers ${err}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 async addOrUpdateController(model: ControllerModel, password: string, refreshTree = true): Promise<void> {
|
||||||
|
const controllerNode = this._controllerNodes.find(node => model.equals(node.model));
|
||||||
|
if (controllerNode) {
|
||||||
|
controllerNode.model.info = model.info;
|
||||||
|
} else {
|
||||||
|
this._controllerNodes.push(new ControllerTreeNode(model, this._context));
|
||||||
|
}
|
||||||
|
await this.updatePassword(model, password);
|
||||||
|
if (refreshTree) {
|
||||||
|
this._onDidChangeTreeData.fire(undefined);
|
||||||
|
}
|
||||||
|
await this.saveControllers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeController(controllerNode: ControllerTreeNode): Promise<void> {
|
||||||
|
this._controllerNodes = this._controllerNodes.filter(node => node !== controllerNode);
|
||||||
|
this._onDidChangeTreeData.fire(undefined);
|
||||||
|
await this.saveControllers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPassword(info: ControllerInfo): Promise<string> {
|
||||||
|
const provider = await this._credentialsProvider;
|
||||||
|
const credential = await provider.readCredential(getCredentialId(info));
|
||||||
|
return credential.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the specified node, or the entire tree if node is undefined
|
||||||
|
* @param node The node to refresh, or undefined for the whole tree
|
||||||
|
*/
|
||||||
|
public refreshNode(node: TreeNode | undefined): void {
|
||||||
|
this._onDidChangeTreeData.fire(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updatePassword(model: ControllerModel, password: string): Promise<void> {
|
||||||
|
const provider = await this._credentialsProvider;
|
||||||
|
if (model.info.rememberPassword) {
|
||||||
|
provider.saveCredential(getCredentialId(model.info), password);
|
||||||
|
} else {
|
||||||
|
provider.deleteCredential(getCredentialId(model.info));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadSavedControllers(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const controllerMementos: ControllerInfo[] = this._context.globalState.get(mementoToken) || [];
|
||||||
|
this._controllerNodes = controllerMementos.map(memento => {
|
||||||
|
const controllerModel = new ControllerModel(this, memento);
|
||||||
|
return new ControllerTreeNode(controllerModel, this._context);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this._loading = false;
|
||||||
|
this._onDidChangeTreeData.fire(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveControllers(): Promise<void> {
|
||||||
|
await this._context.globalState.update(mementoToken, this._controllerNodes.map(node => node.model.info));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCredentialId(info: ControllerInfo): string {
|
||||||
|
return `${info.url}::${info.username}`;
|
||||||
|
}
|
||||||
@@ -1,59 +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 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,6 +13,8 @@ import { ControllerDashboard } from '../dashboards/controller/controllerDashboar
|
|||||||
import { PostgresModel } from '../../models/postgresModel';
|
import { PostgresModel } from '../../models/postgresModel';
|
||||||
import { parseInstanceName } from '../../common/utils';
|
import { parseInstanceName } from '../../common/utils';
|
||||||
import { MiaaModel } from '../../models/miaaModel';
|
import { MiaaModel } from '../../models/miaaModel';
|
||||||
|
import { Deferred } from '../../common/promise';
|
||||||
|
import { RefreshTreeNode } from './refreshTreeNode';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The TreeNode for displaying an Azure Arc Controller
|
* The TreeNode for displaying an Azure Arc Controller
|
||||||
@@ -20,19 +22,31 @@ import { MiaaModel } from '../../models/miaaModel';
|
|||||||
export class ControllerTreeNode extends TreeNode {
|
export class ControllerTreeNode extends TreeNode {
|
||||||
|
|
||||||
private _children: TreeNode[] = [];
|
private _children: TreeNode[] = [];
|
||||||
|
private _childrenRefreshPromise = new Deferred();
|
||||||
|
|
||||||
constructor(private _model: ControllerModel, private _context: vscode.ExtensionContext) {
|
constructor(public model: ControllerModel, private _context: vscode.ExtensionContext) {
|
||||||
super(_model.controllerUrl, vscode.TreeItemCollapsibleState.Collapsed, ResourceType.dataControllers);
|
super(model.info.url, vscode.TreeItemCollapsibleState.Collapsed, ResourceType.dataControllers);
|
||||||
_model.onRegistrationsUpdated(registrations => this.refreshChildren(registrations));
|
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[]> {
|
public async getChildren(): Promise<TreeNode[]> {
|
||||||
|
// First reset our deferred promise so we're sure we'll get the refreshed children
|
||||||
|
this._childrenRefreshPromise = new Deferred();
|
||||||
|
try {
|
||||||
|
await this.model.refresh();
|
||||||
|
await this._childrenRefreshPromise.promise;
|
||||||
|
} catch (err) {
|
||||||
|
// Couldn't get the children and TreeView doesn't have a way to collapse a node
|
||||||
|
// in a way that will refetch its children when expanded again so instead we
|
||||||
|
// display a tempory node that will prompt the user to re-enter credentials
|
||||||
|
return [new RefreshTreeNode(this)];
|
||||||
|
}
|
||||||
|
|
||||||
return this._children;
|
return this._children;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openDashboard(): Promise<void> {
|
public async openDashboard(): Promise<void> {
|
||||||
const controllerDashboard = new ControllerDashboard(this._model);
|
const controllerDashboard = new ControllerDashboard(this.model);
|
||||||
await controllerDashboard.showDashboard();
|
await controllerDashboard.showDashboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,13 +58,14 @@ export class ControllerTreeNode extends TreeNode {
|
|||||||
}
|
}
|
||||||
switch (registration.instanceType) {
|
switch (registration.instanceType) {
|
||||||
case ResourceType.postgresInstances:
|
case ResourceType.postgresInstances:
|
||||||
const postgresModel = new PostgresModel(this._model.controllerUrl, this._model.auth, registration.instanceNamespace, parseInstanceName(registration.instanceName));
|
const postgresModel = new PostgresModel(this.model.info.url, this.model.auth!, registration.instanceNamespace, parseInstanceName(registration.instanceName));
|
||||||
return new PostgresTreeNode(postgresModel, this._model, this._context);
|
return new PostgresTreeNode(postgresModel, this.model, this._context);
|
||||||
case ResourceType.sqlManagedInstances:
|
case ResourceType.sqlManagedInstances:
|
||||||
const miaaModel = new MiaaModel(this._model.controllerUrl, this._model.auth, registration.instanceNamespace, parseInstanceName(registration.instanceName));
|
const miaaModel = new MiaaModel(this.model.info.url, this.model.auth!, registration.instanceNamespace, parseInstanceName(registration.instanceName));
|
||||||
return new MiaaTreeNode(miaaModel, this._model);
|
return new MiaaTreeNode(miaaModel, this.model);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}).filter(item => item); // filter out invalid nodes (controllers or ones without required properties)
|
}).filter(item => item); // filter out invalid nodes (controllers or ones without required properties)
|
||||||
|
this._childrenRefreshPromise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
extensions/arc/src/ui/tree/refreshTreeNode.ts
Normal file
25
extensions/arc/src/ui/tree/refreshTreeNode.ts
Normal 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 * as loc from '../../localizedConstants';
|
||||||
|
import { TreeNode } from './treeNode';
|
||||||
|
import { refreshActionId } from '../../constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A placeholder TreeNode to display when credentials weren't entered
|
||||||
|
*/
|
||||||
|
export class RefreshTreeNode extends TreeNode {
|
||||||
|
|
||||||
|
constructor(private _parent: TreeNode) {
|
||||||
|
super(loc.refreshToEnterCredentials, vscode.TreeItemCollapsibleState.None, 'refresh');
|
||||||
|
}
|
||||||
|
|
||||||
|
public command: vscode.Command = {
|
||||||
|
command: refreshActionId,
|
||||||
|
title: loc.refreshToEnterCredentials,
|
||||||
|
arguments: [this._parent]
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -322,6 +322,11 @@ charenc@~0.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
|
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
|
||||||
integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
|
integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
|
||||||
|
|
||||||
|
circular-json@^0.3.1:
|
||||||
|
version "0.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
|
||||||
|
integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==
|
||||||
|
|
||||||
color-convert@^1.9.0:
|
color-convert@^1.9.0:
|
||||||
version "1.9.3"
|
version "1.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||||
@@ -711,7 +716,7 @@ jsprim@^1.2.2:
|
|||||||
json-schema "0.2.3"
|
json-schema "0.2.3"
|
||||||
verror "1.10.0"
|
verror "1.10.0"
|
||||||
|
|
||||||
lodash@^4.16.4, lodash@^4.17.13:
|
lodash@^4.16.4, lodash@^4.17.13, lodash@^4.17.4:
|
||||||
version "4.17.15"
|
version "4.17.15"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||||
@@ -861,6 +866,11 @@ pify@^4.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
|
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
|
||||||
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
|
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
|
||||||
|
|
||||||
|
postinstall-build@^5.0.1:
|
||||||
|
version "5.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7"
|
||||||
|
integrity sha512-vPvPe8TKgp4FLgY3+DfxCE5PIfoXBK2lyLfNCxsRbDsV6vS4oU5RG/IWxrblMn6heagbnMED3MemUQllQ2bQUg==
|
||||||
|
|
||||||
psl@^1.1.28:
|
psl@^1.1.28:
|
||||||
version "1.8.0"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
|
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
|
||||||
@@ -1068,6 +1078,15 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
|||||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||||
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
|
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
|
||||||
|
|
||||||
|
typemoq@2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/typemoq/-/typemoq-2.1.0.tgz#4452ce360d92cf2a1a180f0c29de2803f87af1e8"
|
||||||
|
integrity sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw==
|
||||||
|
dependencies:
|
||||||
|
circular-json "^0.3.1"
|
||||||
|
lodash "^4.17.4"
|
||||||
|
postinstall-build "^5.0.1"
|
||||||
|
|
||||||
uri-js@^4.2.2:
|
uri-js@^4.2.2:
|
||||||
version "4.2.2"
|
version "4.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
|
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ echo *** starting agent tests ***
|
|||||||
echo ****************************
|
echo ****************************
|
||||||
call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\agent --extensionTestsPath=%~dp0\..\extensions\agent\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu
|
call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\agent --extensionTestsPath=%~dp0\..\extensions\agent\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu
|
||||||
|
|
||||||
|
echo **************************
|
||||||
|
echo *** starting arc tests ***
|
||||||
|
echo **************************
|
||||||
|
call "%INTEGRATION_TEST_ELECTRON_PATH%" --extensionDevelopmentPath=%~dp0\..\extensions\arc --extensionTestsPath=%~dp0\..\extensions\arc\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 --disable-telemetry --disable-crash-reporter --disable-updates --nogpu
|
||||||
|
|
||||||
echo ********************************
|
echo ********************************
|
||||||
echo *** starting azurecore tests ***
|
echo *** starting azurecore tests ***
|
||||||
echo ********************************
|
echo ********************************
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ echo *** starting agent tests ***
|
|||||||
echo ****************************
|
echo ****************************
|
||||||
"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/agent --extensionTestsPath=$ROOT/extensions/agent/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu
|
"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/agent --extensionTestsPath=$ROOT/extensions/agent/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu
|
||||||
|
|
||||||
|
echo **************************
|
||||||
|
echo *** starting arc tests ***
|
||||||
|
echo **************************
|
||||||
|
"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --extensionDevelopmentPath=$ROOT/extensions/arc --extensionTestsPath=$ROOT/extensions/arc/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR --disable-telemetry --disable-crash-reporter --disable-updates --nogpu
|
||||||
|
|
||||||
echo ********************************
|
echo ********************************
|
||||||
echo *** starting azurecore tests ***
|
echo *** starting azurecore tests ***
|
||||||
echo ********************************
|
echo ********************************
|
||||||
|
|||||||
Reference in New Issue
Block a user