diff --git a/extensions/arc/package.json b/extensions/arc/package.json index 68cd0ca8f8..ff75e39263 100644 --- a/extensions/arc/package.json +++ b/extensions/arc/package.json @@ -56,6 +56,10 @@ { "command": "arc.refresh", "title": "%command.refresh.title%" + }, + { + "command": "arc.editConnection", + "title": "%command.editConnection.title%" } ], "menus": { @@ -71,6 +75,10 @@ { "command": "arc.refresh", "when": "false" + }, + { + "command": "arc.editConnection", + "when": "false" } ], "view/title": [ @@ -92,14 +100,19 @@ "group": "navigation@1" }, { - "command": "arc.refresh", + "command": "arc.editConnection", "when": "view == azureArc && viewItem == dataControllers", "group": "navigation@2" }, { - "command": "arc.removeController", + "command": "arc.refresh", "when": "view == azureArc && viewItem == dataControllers", "group": "navigation@3" + }, + { + "command": "arc.removeController", + "when": "view == azureArc && viewItem == dataControllers", + "group": "navigation@4" } ] }, @@ -794,6 +807,7 @@ }, "dependencies": { "request": "^2.88.0", + "uuid": "^8.3.0", "vscode-nls": "^4.1.2" }, "devDependencies": { @@ -801,6 +815,7 @@ "@types/node": "^12.11.7", "@types/request": "^2.48.3", "@types/sinon": "^9.0.4", + "@types/uuid": "^8.3.0", "mocha": "^5.2.0", "mocha-junit-reporter": "^1.17.0", "mocha-multi-reporters": "^1.1.7", diff --git a/extensions/arc/package.nls.json b/extensions/arc/package.nls.json index cd04b46f4c..8ac95ad7b1 100644 --- a/extensions/arc/package.nls.json +++ b/extensions/arc/package.nls.json @@ -9,6 +9,7 @@ "command.connectToController.title": "Connect to Existing Azure Arc Controller", "command.removeController.title": "Remove Controller", "command.refresh.title": "Refresh", + "command.editConnection.title": "Edit Connection", "arc.openDashboard": "Manage", "resource.type.azure.arc.display.name": "Azure Arc data controller", diff --git a/extensions/arc/src/extension.ts b/extensions/arc/src/extension.ts index 8369e3d319..d1cb071d74 100644 --- a/extensions/arc/src/extension.ts +++ b/extensions/arc/src/extension.ts @@ -44,6 +44,15 @@ export async function activate(context: vscode.ExtensionContext): Promise await treeNode.openDashboard().catch(err => vscode.window.showErrorMessage(loc.openDashboardFailed(err))); }); + vscode.commands.registerCommand('arc.editConnection', async (treeNode: ControllerTreeNode) => { + const dialog = new ConnectToControllerDialog(treeDataProvider); + dialog.showDialog(treeNode.model.info, await treeDataProvider.getPassword(treeNode.model.info)); + const model = await dialog.waitForClose(); + if (model) { + await treeDataProvider.addOrUpdateController(model.controllerModel, model.password, true); + } + }); + await checkArcDeploymentExtension(); } diff --git a/extensions/arc/src/models/controllerModel.ts b/extensions/arc/src/models/controllerModel.ts index 514dcca85d..cb93934909 100644 --- a/extensions/arc/src/models/controllerModel.ts +++ b/extensions/arc/src/models/controllerModel.ts @@ -12,6 +12,7 @@ import * as loc from '../localizedConstants'; import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog'; export type ControllerInfo = { + id: string, url: string, name: string, username: string, @@ -41,19 +42,30 @@ export class ControllerModel { private readonly _onConfigUpdated = new vscode.EventEmitter(); private readonly _onEndpointsUpdated = new vscode.EventEmitter(); private readonly _onRegistrationsUpdated = new vscode.EventEmitter(); + private readonly _onInfoUpdated = new vscode.EventEmitter(); public onConfigUpdated = this._onConfigUpdated.event; public onEndpointsUpdated = this._onEndpointsUpdated.event; public onRegistrationsUpdated = this._onRegistrationsUpdated.event; + public onInfoUpdated = this._onInfoUpdated.event; public configLastUpdated?: Date; public endpointsLastUpdated?: Date; public registrationsLastUpdated?: Date; - constructor(public treeDataProvider: AzureArcTreeDataProvider, public info: ControllerInfo, private _password?: string) { + constructor(public treeDataProvider: AzureArcTreeDataProvider, private _info: ControllerInfo, private _password?: string) { this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports; } + public get info(): ControllerInfo { + return this._info; + } + + public set info(value: ControllerInfo) { + this._info = value; + this._onInfoUpdated.fire(this._info); + } + /** * Calls azdata login to set the context to this controller * @param promptReconnect @@ -187,15 +199,6 @@ export class ControllerModel { */ } - /** - * 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; - } - /** * property to for use a display label for this controller */ diff --git a/extensions/arc/src/test/mocks/fakeControllerModel.ts b/extensions/arc/src/test/mocks/fakeControllerModel.ts new file mode 100644 index 0000000000..ae78022fe9 --- /dev/null +++ b/extensions/arc/src/test/mocks/fakeControllerModel.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { v4 as uuid } from 'uuid'; +import { ControllerModel, ControllerInfo } from '../../models/controllerModel'; +import { AzureArcTreeDataProvider } from '../../ui/tree/azureArcTreeDataProvider'; + +export class FakeControllerModel extends ControllerModel { + + constructor(treeDataProvider?: AzureArcTreeDataProvider, info?: Partial, password?: string) { + const _info: ControllerInfo = Object.assign({ id: uuid(), url: '', name: '', username: '', rememberPassword: false, resources: [] }, info); + super(treeDataProvider!, _info, password); + } + +} diff --git a/extensions/arc/src/test/models/controllerModel.test.ts b/extensions/arc/src/test/models/controllerModel.test.ts index 8abb65a720..fef79b5408 100644 --- a/extensions/arc/src/test/models/controllerModel.test.ts +++ b/extensions/arc/src/test/models/controllerModel.test.ts @@ -9,8 +9,9 @@ import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; import * as vscode from 'vscode'; import * as should from 'should'; +import { v4 as uuid } from 'uuid'; import { ConnectToControllerDialog } from '../../ui/dialogs/connectControllerDialog'; -import { ControllerModel } from '../../models/controllerModel'; +import { ControllerModel, ControllerInfo } from '../../models/controllerModel'; import { AzureArcTreeDataProvider } from '../../ui/tree/azureArcTreeDataProvider'; import { UserCancelledError } from '../../common/utils'; @@ -36,7 +37,7 @@ describe('ControllerModel', function (): void { it('Rejected with expected error when user cancels', async function (): Promise { // Returning an undefined model here indicates that the dialog closed without clicking "Ok" - usually through the user clicking "Cancel" sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(undefined)); - const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }); + const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }); await should(model.azdataLogin()).be.rejectedWith(new UserCancelledError()); }); @@ -55,7 +56,7 @@ describe('ControllerModel', function (): void { azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object); sinon.stub(vscode.extensions, 'getExtension').returns({ exports: azdataExtApiMock.object }); - const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }); + const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }); await model.azdataLogin(); azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password), TypeMoq.Times.once()); @@ -78,10 +79,10 @@ describe('ControllerModel', function (): void { sinon.stub(vscode.extensions, 'getExtension').returns({ exports: azdataExtApiMock.object }); // Set up dialog to return new model with our password - const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password); + const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password); sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password })); - const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }); + const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }); await model.azdataLogin(); azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password), TypeMoq.Times.once()); @@ -103,10 +104,10 @@ describe('ControllerModel', function (): void { sinon.stub(vscode.extensions, 'getExtension').returns({ exports: azdataExtApiMock.object }); // Set up dialog to return new model with our new password from the reprompt - const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password); + const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password); const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password })); - const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }); + const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }); await model.azdataLogin(true); should(waitForCloseStub.called).be.true('waitForClose should have been called'); @@ -129,16 +130,72 @@ describe('ControllerModel', function (): void { sinon.stub(vscode.extensions, 'getExtension').returns({ exports: azdataExtApiMock.object }); // Set up dialog to return new model with our new password from the reprompt - const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password); + const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password); const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password })); // Set up original model with a password - const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, 'originalPassword'); + const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, 'originalPassword'); await model.azdataLogin(true); should(waitForCloseStub.called).be.true('waitForClose should have been called'); azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password), TypeMoq.Times.once()); }); + + it('Model values are updated correctly when modified during reconnect', async function (): Promise { + const treeDataProvider = new AzureArcTreeDataProvider(mockExtensionContext.object); + + // Set up cred store to return a password to start with + const credProviderMock = TypeMoq.Mock.ofType(); + credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: 'originalPassword' })); + // Need to setup then when Promise.resolving a mocked object : https://github.com/florinn/typemoq/issues/66 + credProviderMock.setup((x: any) => x.then).returns(() => undefined); + sinon.stub(azdata.credentials, 'getProvider').returns(Promise.resolve(credProviderMock.object)); + + const azdataExtApiMock = TypeMoq.Mock.ofType(); + const azdataMock = TypeMoq.Mock.ofType(); + azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object); + sinon.stub(vscode.extensions, 'getExtension').returns({ exports: azdataExtApiMock.object }); + + // Add existing model to provider + const originalPassword = 'originalPassword'; + const model = new ControllerModel( + treeDataProvider, + { + id: uuid(), + url: '127.0.0.1', + username: 'admin', + name: 'arc', + rememberPassword: false, + resources: [] + }, + originalPassword + ); + await treeDataProvider.addOrUpdateController(model, originalPassword); + + const newInfo: ControllerInfo = { + id: model.info.id, // The ID stays the same since we're just re-entering information for the same model + url: 'newUrl', + username: 'newUser', + name: 'newName', + rememberPassword: true, + resources: [] + }; + const newPassword = 'newPassword'; + // Set up dialog to return new model with our new password from the reprompt + const newModel = new ControllerModel( + treeDataProvider, + newInfo, + newPassword); + const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve( + { controllerModel: newModel, password: newPassword })); + + await model.azdataLogin(true); + should(waitForCloseStub.called).be.true('waitForClose should have been called'); + should((await treeDataProvider.getChildren()).length).equal(1, 'Tree Data provider should still only have 1 node'); + should(model.info).deepEqual(newInfo, 'Model info should have been updated'); + }); + }); }); diff --git a/extensions/arc/src/test/ui/dialogs/connectControllerDialog.test.ts b/extensions/arc/src/test/ui/dialogs/connectControllerDialog.test.ts index 0f379da0cb..0a7a2a9ee5 100644 --- a/extensions/arc/src/test/ui/dialogs/connectControllerDialog.test.ts +++ b/extensions/arc/src/test/ui/dialogs/connectControllerDialog.test.ts @@ -8,6 +8,7 @@ import * as sinon from 'sinon'; import { ControllerInfo, ControllerModel } from '../../../models/controllerModel'; import { ConnectToControllerDialog } from '../../../ui/dialogs/connectControllerDialog'; import * as loc from '../../../localizedConstants'; +import { v4 as uuid } from 'uuid'; describe('ConnectControllerDialog', function (): void { afterEach(function (): void { @@ -30,7 +31,7 @@ describe('ConnectControllerDialog', function (): void { it('validate returns false if controller refresh fails', async function (): Promise { sinon.stub(ControllerModel.prototype, 'refresh').returns(Promise.reject('Controller refresh failed')); const connectControllerDialog = new ConnectToControllerDialog(undefined!); - const info = { url: 'https://127.0.0.1:30080', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }; + const info = { id: uuid(), url: 'https://127.0.0.1:30080', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }; connectControllerDialog.showDialog(info, 'pwd'); await connectControllerDialog.isInitialized; const validateResult = await connectControllerDialog.validate(); @@ -39,36 +40,36 @@ describe('ConnectControllerDialog', function (): void { it('validate replaces http with https', async function (): Promise { await validateConnectControllerDialog( - { url: 'http://127.0.0.1:30081', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, + { id: uuid(), url: 'http://127.0.0.1:30081', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, 'https://127.0.0.1:30081'); }); it('validate appends https if missing', async function (): Promise { - await validateConnectControllerDialog({ url: '127.0.0.1:30080', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, + await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1:30080', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, 'https://127.0.0.1:30080'); }); it('validate appends default port if missing', async function (): Promise { - await validateConnectControllerDialog({ url: 'https://127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, + await validateConnectControllerDialog({ id: uuid(), url: 'https://127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, 'https://127.0.0.1:30080'); }); it('validate appends both port and https if missing', async function (): Promise { - await validateConnectControllerDialog({ url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, + await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, 'https://127.0.0.1:30080'); }); for (const name of ['', undefined]) { it.skip(`validate display name gets set to arc instance name for user chosen name of:${name}`, async function (): Promise { await validateConnectControllerDialog( - { url: 'http://127.0.0.1:30081', name: name!, username: 'sa', rememberPassword: true, resources: [] }, + { id: uuid(), url: 'http://127.0.0.1:30081', name: name!, username: 'sa', rememberPassword: true, resources: [] }, 'https://127.0.0.1:30081'); }); } it.skip(`validate display name gets set to default data controller name for user chosen name of:'' and instanceName in explicably returned as undefined from the controller endpoint`, async function (): Promise { await validateConnectControllerDialog( - { url: 'http://127.0.0.1:30081', name: '', username: 'sa', rememberPassword: true, resources: [] }, + { id: uuid(), url: 'http://127.0.0.1:30081', name: '', username: 'sa', rememberPassword: true, resources: [] }, 'https://127.0.0.1:30081', undefined); }); diff --git a/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts b/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts index 263a4f2e56..9937122cfc 100644 --- a/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts +++ b/extensions/arc/src/test/ui/tree/azureArcTreeDataProvider.test.ts @@ -6,10 +6,12 @@ import 'mocha'; import * as should from 'should'; import * as TypeMoq from 'typemoq'; +import { v4 as uuid } from 'uuid'; import * as vscode from 'vscode'; -import { ControllerModel } from '../../../models/controllerModel'; +import { ControllerModel, ControllerInfo } from '../../../models/controllerModel'; import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider'; import { ControllerTreeNode } from '../../../ui/tree/controllerTreeNode'; +import { FakeControllerModel } from '../../mocks/fakeControllerModel'; describe('AzureArcTreeDataProvider tests', function (): void { let treeDataProvider: AzureArcTreeDataProvider; @@ -27,15 +29,17 @@ describe('AzureArcTreeDataProvider tests', function (): 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(); - await treeDataProvider.addOrUpdateController(controllerModelMock.object, ''); + const controllerModel = new FakeControllerModel(); + await treeDataProvider.addOrUpdateController(controllerModel, ''); + children = await treeDataProvider.getChildren(); should(children.length).equal(1, 'Controller node should be added correctly'); // Add a couple more - const controllerModelMock2 = TypeMoq.Mock.ofType(); - const controllerModelMock3 = TypeMoq.Mock.ofType(); - await treeDataProvider.addOrUpdateController(controllerModelMock2.object, ''); - await treeDataProvider.addOrUpdateController(controllerModelMock3.object, ''); + const controllerModel2 = new FakeControllerModel(); + const controllerModel3 = new FakeControllerModel(); + await treeDataProvider.addOrUpdateController(controllerModel2, ''); + await treeDataProvider.addOrUpdateController(controllerModel3, ''); + children = await treeDataProvider.getChildren(); should(children.length).equal(3, 'Additional Controller nodes should be added correctly'); }); @@ -43,7 +47,7 @@ describe('AzureArcTreeDataProvider tests', function (): 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', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }); + const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }); await treeDataProvider.addOrUpdateController(controllerModel, ''); should(children.length).equal(1, 'Controller node should be added correctly'); await treeDataProvider.addOrUpdateController(controllerModel, ''); @@ -54,14 +58,16 @@ describe('AzureArcTreeDataProvider tests', function (): 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', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }); + const originalInfo: ControllerInfo = { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }; + const controllerModel = new ControllerModel(treeDataProvider, originalInfo); await treeDataProvider.addOrUpdateController(controllerModel, ''); should(children.length).equal(1, 'Controller node should be added correctly'); - should((children[0]).model.info.rememberPassword).be.true('Info was not set correctly initially'); - const controllerModel2 = new ControllerModel(treeDataProvider, { url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: false, resources: [] }); + should((children[0]).model.info).deepEqual(originalInfo); + const newInfo = { id: originalInfo.id, url: '1.1.1.1', name: 'new-name', username: 'admin', rememberPassword: false, resources: [] }; + const controllerModel2 = new ControllerModel(treeDataProvider, newInfo); await treeDataProvider.addOrUpdateController(controllerModel2, ''); should(children.length).equal(1, 'Shouldn\'t add duplicate controller node'); - should((children[0]).model.info.rememberPassword).be.false('Info was not updated correctly'); + should((children[0]).model.info).deepEqual(newInfo); }); }); @@ -82,8 +88,8 @@ describe('AzureArcTreeDataProvider tests', function (): void { describe('removeController', function (): void { it('removing a controller should work as expected', async function (): Promise { treeDataProvider['_loading'] = false; - const controllerModel = new ControllerModel(treeDataProvider, { url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }); - const controllerModel2 = new ControllerModel(treeDataProvider, { url: '127.0.0.2', name: 'my-arc', username: 'cloudsa', rememberPassword: true, resources: [] }); + const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }); + const controllerModel2 = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.2', name: 'my-arc', username: 'cloudsa', rememberPassword: true, resources: [] }); await treeDataProvider.addOrUpdateController(controllerModel, ''); await treeDataProvider.addOrUpdateController(controllerModel2, ''); const children = (await treeDataProvider.getChildren()); diff --git a/extensions/arc/src/ui/dialogs/connectControllerDialog.ts b/extensions/arc/src/ui/dialogs/connectControllerDialog.ts index ddd9d92919..bc8524c11c 100644 --- a/extensions/arc/src/ui/dialogs/connectControllerDialog.ts +++ b/extensions/arc/src/ui/dialogs/connectControllerDialog.ts @@ -5,6 +5,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; +import { v4 as uuid } from 'uuid'; import { Deferred } from '../../common/promise'; import * as loc from '../../localizedConstants'; import { ControllerInfo, ControllerModel } from '../../models/controllerModel'; @@ -23,11 +24,14 @@ export class ConnectToControllerDialog extends InitializingComponent { private _completionPromise = new Deferred(); + private _id!: string; + constructor(private _treeDataProvider: AzureArcTreeDataProvider) { super(); } public showDialog(controllerInfo?: ControllerInfo, password?: string): azdata.window.Dialog { + this._id = controllerInfo?.id ?? uuid(); const dialog = azdata.window.createModelViewDialog(loc.connectToController); dialog.cancelButton.onClick(() => this.handleCancel()); dialog.registerContent(async view => { @@ -115,6 +119,7 @@ export class ConnectToControllerDialog extends InitializingComponent { url = `${url}:30080`; } const controllerInfo: ControllerInfo = { + id: this._id, url: url, name: this.nameInputBox.value ?? '', username: this.usernameInputBox.value, diff --git a/extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts b/extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts index 1c5e325786..5a91bc1b4e 100644 --- a/extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts +++ b/extensions/arc/src/ui/tree/azureArcTreeDataProvider.ts @@ -65,21 +65,27 @@ export class AzureArcTreeDataProvider implements vscode.TreeDataProvider model.equals(node.model)); + return this._controllerNodes.find(node => model.info.id === node.model.info.id); } public async removeController(controllerNode: ControllerTreeNode): Promise { this._controllerNodes = this._controllerNodes.filter(node => node !== controllerNode); + await this.deletePassword(controllerNode.model.info); this._onDidChangeTreeData.fire(undefined); await this.saveControllers(); } public async getPassword(info: ControllerInfo): Promise { const provider = await this._credentialsProvider; - const credential = await provider.readCredential(getCredentialId(info)); + const credential = await provider.readCredential(info.id); return credential.password; } + private async deletePassword(info: ControllerInfo): Promise { + const provider = await this._credentialsProvider; + await provider.deleteCredential(info.id); + } + /** * Refreshes the specified node, or the entire tree if node is undefined * @param node The node to refresh, or undefined for the whole tree @@ -91,9 +97,9 @@ export class AzureArcTreeDataProvider implements vscode.TreeDataProvider { const provider = await this._credentialsProvider; if (model.info.rememberPassword) { - provider.saveCredential(getCredentialId(model.info), password); + await provider.saveCredential(model.info.id, password); } else { - provider.deleteCredential(getCredentialId(model.info)); + await provider.deleteCredential(model.info.id); } } @@ -136,7 +142,3 @@ export class AzureArcTreeDataProvider implements vscode.TreeDataProvider { + this.label = model.label; + }); } public async getChildren(): Promise { diff --git a/extensions/arc/yarn.lock b/extensions/arc/yarn.lock index cc9b672e80..f87395b60d 100644 --- a/extensions/arc/yarn.lock +++ b/extensions/arc/yarn.lock @@ -265,6 +265,11 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d" integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A== +"@types/uuid@^8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f" + integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ== + ajv@^6.5.5: version "6.12.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" @@ -1203,6 +1208,11 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" + integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== + verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"