From 51b0c69e093359c1e68f0c0accda077e04f79ad3 Mon Sep 17 00:00:00 2001 From: Dharini Date: Tue, 28 Oct 2025 18:04:58 -0700 Subject: [PATCH 1/3] =?UTF-8?q?chore:=20=F0=9F=A4=96=20add=20rdp=20client?= =?UTF-8?q?=20launch=20functionality=20in=20target=20details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17881 --- .../scopes/scope/projects/targets/target.js | 24 ++++++ .../scopes/scope/projects/targets/target.hbs | 32 +++++--- .../projects/targets/target-test.js | 79 +++++++++++++++++++ 3 files changed, 124 insertions(+), 11 deletions(-) diff --git a/ui/desktop/app/controllers/scopes/scope/projects/targets/target.js b/ui/desktop/app/controllers/scopes/scope/projects/targets/target.js index d060a8ef9a..50ca19fa42 100644 --- a/ui/desktop/app/controllers/scopes/scope/projects/targets/target.js +++ b/ui/desktop/app/controllers/scopes/scope/projects/targets/target.js @@ -15,6 +15,7 @@ export default class ScopesScopeProjectsTargetsTargetController extends Controll @service store; @service confirm; + @service rdp; // =attributes @@ -48,4 +49,27 @@ export default class ScopesScopeProjectsTargetsTargetController extends Controll }); } } + + /** + * Launch method that calls parent quickConnectAndLaunchRdp method and handles + * connection errors unique to this route + * @param {TargetModel} target + */ + @action + async connectAndLaunchRdp(target) { + try { + await this.targets.quickConnectAndLaunchRdp(target); + } catch (error) { + this.isConnectionError = true; + this.confirm + .confirm(error.message) + // Retry + .then(() => this.connectAndLaunchRdp(target)) + .catch(() => { + // Reset the flag as this was user initiated and we're not + // in a transition + this.isConnectionError = false; + }); + } + } } diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs index e151496f32..2e78af4778 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs @@ -15,17 +15,27 @@ {{#if (can 'connect target' @model.target)}} - + {{#if (and @model.target.isRDP this.rdp.isPreferredRdpClientSet)}} + + {{else}} + + {{/if}} {{/if}} diff --git a/ui/desktop/tests/acceptance/projects/targets/target-test.js b/ui/desktop/tests/acceptance/projects/targets/target-test.js index 2d37f32333..049974f346 100644 --- a/ui/desktop/tests/acceptance/projects/targets/target-test.js +++ b/ui/desktop/tests/acceptance/projects/targets/target-test.js @@ -19,6 +19,7 @@ import { authenticateSession } from 'ember-simple-auth/test-support'; import WindowMockIPC from '../../../helpers/window-mock-ipc'; import setupStubs from 'api/test-support/handlers/cache-daemon-search'; import { setRunOptions } from 'ember-a11y-testing/test-support'; +import { TYPE_TARGET_RDP } from 'api/models/target'; module('Acceptance | projects | targets | target', function (hooks) { setupApplicationTest(hooks); @@ -28,6 +29,7 @@ module('Acceptance | projects | targets | target', function (hooks) { const TARGET_RESOURCE_LINK = (id) => `[data-test-visit-target="${id}"]`; const TARGET_TABLE_CONNECT_BUTTON = (id) => `[data-test-targets-connect-button="${id}"]`; + const TARGET_TABLE_DETAILS_OPEN_BUTTON = `[data-test-target-detail-open-button]`; const TARGET_CONNECT_BUTTON = '[data-test-target-detail-connect-button]'; const TARGET_HOST_SOURCE_CONNECT_BUTTON = (id) => `[data-test-target-connect-button=${id}]`; @@ -576,6 +578,7 @@ module('Acceptance | projects | targets | target', function (hooks) { assert.strictEqual(currentURL(), urls.targetWithOneHost); assert.dom('.aliases').exists(); }); + test('user can connect to a target without read permissions for host-set', async function (assert) { setRunOptions({ rules: { @@ -631,4 +634,80 @@ module('Acceptance | projects | targets | target', function (hooks) { assert.dom(APP_STATE_TITLE).hasText('Connected'); }); + + test('shows "Open" button for RDP target with preferred client', async function (assert) { + let rdpService = this.owner.lookup('service:rdp'); + rdpService.preferredRdpClient = 'windows-app'; + instances.target.update({ type: TYPE_TARGET_RDP }); + + this.stubCacheDaemonSearch(); + + await visit(urls.target); + + assert.dom(TARGET_TABLE_DETAILS_OPEN_BUTTON).exists(); + assert.dom(TARGET_TABLE_DETAILS_OPEN_BUTTON).hasText('Open'); + assert.dom('[data-test-icon=external-link]').exists(); + }); + + test('shows "Connect" button for RDP target without preferred client', async function (assert) { + let rdpService = this.owner.lookup('service:rdp'); + rdpService.preferredRdpClient = null; + instances.target.update({ type: TYPE_TARGET_RDP }); + + this.stubCacheDaemonSearch(); + + await visit(urls.target); + + assert.dom(TARGET_CONNECT_BUTTON).exists(); + assert.dom(TARGET_CONNECT_BUTTON).hasText('Connect'); + }); + + test('clicking `open` button for RDP target triggers launchRdpClient', async function (assert) { + let rdpService = this.owner.lookup('service:rdp'); + rdpService.preferredRdpClient = 'windows-app'; + instances.target.update({ type: TYPE_TARGET_RDP }); + + this.ipcStub.withArgs('cliExists').returns(true); + this.ipcStub.withArgs('connect').returns({ + session_id: instances.session.id, + address: 'a_123', + port: 'p_123', + protocol: 'rdp', + }); + this.stubCacheDaemonSearch(); + this.ipcStub.withArgs('launchRdpClient').resolves(); + + const confirmService = this.owner.lookup('service:confirm'); + confirmService.enabled = true; + + await visit(urls.target); + + await click(TARGET_TABLE_DETAILS_OPEN_BUTTON); + + assert.ok(this.ipcStub.calledWith('launchRdpClient', instances.session.id)); + }); + + test('shows `Connect` button for rdp target without preferred client', async function (assert) { + let rdpService = this.owner.lookup('service:rdp'); + rdpService.preferredRdpClient = null; + instances.target.update({ type: TYPE_TARGET_RDP }); + + this.stubCacheDaemonSearch(); + this.ipcStub.withArgs('cliExists').returns(true); + this.ipcStub.withArgs('connect').returns({ + session_id: instances.session.id, + address: 'a_123', + port: 'p_123', + protocol: 'rdp', + }); + await visit(urls.target); + + assert.dom(TARGET_CONNECT_BUTTON).exists(); + assert.dom(TARGET_CONNECT_BUTTON).hasText('Connect'); + + await click(TARGET_CONNECT_BUTTON); + + assert.ok(this.ipcStub.calledWith('connect')); + assert.notOk(this.ipcStub.calledWith('launchRdpClient')); + }); }); From a000a6fad1f181f2bf31f18bc45bc2377174a044 Mon Sep 17 00:00:00 2001 From: Dharini Date: Wed, 29 Oct 2025 12:34:47 -0700 Subject: [PATCH 2/3] =?UTF-8?q?chore:=20=F0=9F=A4=96=20feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17881 --- .../scopes/scope/projects/targets/target.hbs | 10 ++++++++++ .../acceptance/projects/targets/target-test.js | 14 ++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs index 2e78af4778..b9f5b3547a 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs @@ -16,6 +16,16 @@ {{#if (can 'connect target' @model.target)}} {{#if (and @model.target.isRDP this.rdp.isPreferredRdpClientSet)}} + `[data-test-visit-target="${id}"]`; const TARGET_TABLE_CONNECT_BUTTON = (id) => `[data-test-targets-connect-button="${id}"]`; - const TARGET_TABLE_DETAILS_OPEN_BUTTON = `[data-test-target-detail-open-button]`; + const TARGET_OPEN_BUTTON = `[data-test-target-detail-open-button]`; const TARGET_CONNECT_BUTTON = '[data-test-target-detail-connect-button]'; const TARGET_HOST_SOURCE_CONNECT_BUTTON = (id) => `[data-test-target-connect-button=${id}]`; @@ -635,7 +635,7 @@ module('Acceptance | projects | targets | target', function (hooks) { assert.dom(APP_STATE_TITLE).hasText('Connected'); }); - test('shows "Open" button for RDP target with preferred client', async function (assert) { + test('shows `Open` and `Connect` button for RDP target with preferred client', async function (assert) { let rdpService = this.owner.lookup('service:rdp'); rdpService.preferredRdpClient = 'windows-app'; instances.target.update({ type: TYPE_TARGET_RDP }); @@ -644,9 +644,10 @@ module('Acceptance | projects | targets | target', function (hooks) { await visit(urls.target); - assert.dom(TARGET_TABLE_DETAILS_OPEN_BUTTON).exists(); - assert.dom(TARGET_TABLE_DETAILS_OPEN_BUTTON).hasText('Open'); - assert.dom('[data-test-icon=external-link]').exists(); + assert.dom(TARGET_OPEN_BUTTON).exists(); + assert.dom(TARGET_OPEN_BUTTON).hasText('Open'); + assert.dom(TARGET_CONNECT_BUTTON).exists(); + assert.dom(TARGET_CONNECT_BUTTON).hasText('Connect'); }); test('shows "Connect" button for RDP target without preferred client', async function (assert) { @@ -682,7 +683,7 @@ module('Acceptance | projects | targets | target', function (hooks) { await visit(urls.target); - await click(TARGET_TABLE_DETAILS_OPEN_BUTTON); + await click(TARGET_OPEN_BUTTON); assert.ok(this.ipcStub.calledWith('launchRdpClient', instances.session.id)); }); @@ -704,6 +705,7 @@ module('Acceptance | projects | targets | target', function (hooks) { assert.dom(TARGET_CONNECT_BUTTON).exists(); assert.dom(TARGET_CONNECT_BUTTON).hasText('Connect'); + assert.dom(TARGET_OPEN_BUTTON).doesNotExist(); await click(TARGET_CONNECT_BUTTON); From c2c1a491f64653944281412f2c52220a355c9985 Mon Sep 17 00:00:00 2001 From: Dharini Date: Thu, 30 Oct 2025 15:26:53 -0700 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=F0=9F=90=9B=20remove=20try/catch=20?= =?UTF-8?q?from=20target=20detail=20and=20add=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17881 --- .../scopes/scope/projects/targets/target.js | 18 ++---------------- .../acceptance/projects/targets/target-test.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/ui/desktop/app/controllers/scopes/scope/projects/targets/target.js b/ui/desktop/app/controllers/scopes/scope/projects/targets/target.js index 50ca19fa42..adf1eb5148 100644 --- a/ui/desktop/app/controllers/scopes/scope/projects/targets/target.js +++ b/ui/desktop/app/controllers/scopes/scope/projects/targets/target.js @@ -51,25 +51,11 @@ export default class ScopesScopeProjectsTargetsTargetController extends Controll } /** - * Launch method that calls parent quickConnectAndLaunchRdp method and handles - * connection errors unique to this route + * Launch method that calls parent quickConnectAndLaunchRdp method * @param {TargetModel} target */ @action async connectAndLaunchRdp(target) { - try { - await this.targets.quickConnectAndLaunchRdp(target); - } catch (error) { - this.isConnectionError = true; - this.confirm - .confirm(error.message) - // Retry - .then(() => this.connectAndLaunchRdp(target)) - .catch(() => { - // Reset the flag as this was user initiated and we're not - // in a transition - this.isConnectionError = false; - }); - } + await this.targets.quickConnectAndLaunchRdp(target); } } diff --git a/ui/desktop/tests/acceptance/projects/targets/target-test.js b/ui/desktop/tests/acceptance/projects/targets/target-test.js index 45ee6cb92e..983b54c14a 100644 --- a/ui/desktop/tests/acceptance/projects/targets/target-test.js +++ b/ui/desktop/tests/acceptance/projects/targets/target-test.js @@ -712,4 +712,21 @@ module('Acceptance | projects | targets | target', function (hooks) { assert.ok(this.ipcStub.calledWith('connect')); assert.notOk(this.ipcStub.calledWith('launchRdpClient')); }); + + test('shows confirm modal when quickConnectAndLaunchRdp fails', async function (assert) { + let rdpService = this.owner.lookup('service:rdp'); + rdpService.preferredRdpClient = 'windows-app'; + instances.target.update({ type: TYPE_TARGET_RDP }); + this.stubCacheDaemonSearch(); + + const confirmService = this.owner.lookup('service:confirm'); + confirmService.enabled = true; + + await visit(urls.target); + + await click('[data-test-target-detail-open-button]'); + + // The modal should be visible because cliExists was not stubbed, and the connection failed + assert.dom(HDS_DIALOG_MODAL).isVisible(); + }); });