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:
Charles Gagnon
2020-06-19 14:35:11 -07:00
committed by GitHub
parent c879d77b62
commit f278e2a7a2
17 changed files with 576 additions and 176 deletions

View 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
}
}

View File

@@ -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"
} }
} }

View File

@@ -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"
} }

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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)); }

View File

@@ -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);
}
} }
/** /**

View File

@@ -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('');
});
});

View 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');
});
});
});

View 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;
}
}

View 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}`;
}

View File

@@ -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);
}
}

View File

@@ -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();
} }
} }

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * 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]
};
}

View File

@@ -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"

View File

@@ -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 ********************************

View File

@@ -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 ********************************