feat: 🎸 Add RDP client launch on session details page (#3037)

* feat: 🎸 preferred clients section on settings page

* refactor: 💡 moved rdp calls to app route

* test: 💍 fixed failings tests

* refactor: 💡 addressed comments

* fix: 🐛 fixed failing tests

* refactor: 💡 add mock rdp service calls to tests

* feat: 🎸 adds open button to session details page

* fix: 🐛 translation string

* refactor: 💡 address feedback

* refactor: 💡 address comment

* fix: 🐛 test failure
This commit is contained in:
Priya Patel authored and Dharini committed 2025-11-19 14:22:07 -08:00
1 parent e55ea0b629
commit fb0020c395
4 files changed
+174 -28

No files matched your search

@@ -4,7 +4,43 @@
*/
import Controller, { inject as controller } from '@ember/controller';
import { TYPE_TARGET_RDP } from 'api/models/target';
import { service } from '@ember/service';
import { action } from '@ember/object';
export default class ScopesScopeProjectsSessionsSessionController extends Controller {
@controller('scopes/scope/projects/sessions/index') sessions;
// =services
@service rdp;
@service confirm;
// =getters
/**
* Whether to show the "Open" button for launching the RDP client.
* @returns {boolean}
*/
get showOpenButton() {
return (
this.model.target?.type === TYPE_TARGET_RDP &&
this.rdp.isPreferredRdpClientSet &&
this.model.id
);
}
// =methods
@action
async launchRdpClient() {
try {
await this.rdp.launchRdpClient(this.model.id);
} catch (error) {
this.confirm
.confirm(error.message, { isConnectError: true })
// Retry
.then(() => this.launchRdpClient());
}
}
}
@@ -19,15 +19,27 @@
@icon='loading'
/>
</PH.Badges>
{{#if (can 'cancel session' @model)}}
{{#if (or (can 'cancel session' @model) this.showOpenButton)}}
<PH.Actions>
<Hds::Button
data-test-session-detail-cancel-button
@text={{t 'resources.session.actions.end'}}
@icon='x'
@color='secondary'
{{on 'click' (fn this.sessions.cancelSession @model)}}
/>
{{#if (can 'cancel session' @model)}}
<Hds::Button
data-test-session-detail-cancel-button
@text={{t 'resources.session.actions.end'}}
@icon='x'
@color='secondary'
{{on 'click' (fn this.sessions.cancelSession @model)}}
/>
{{/if}}
{{#if this.showOpenButton}}
<Hds::Button
data-test-session-detail-open-button
@text={{t 'actions.open'}}
@icon='external-link'
@iconPosition='trailing'
@color='primary'
{{on 'click' this.launchRdpClient}}
/>
{{/if}}
</PH.Actions>
{{/if}}
</Hds::PageHeader>
@@ -14,6 +14,7 @@ import { STATUS_SESSION_ACTIVE } from 'api/models/session';
import setupStubs from 'api/test-support/handlers/cache-daemon-search';
import { setRunOptions } from 'ember-a11y-testing/test-support';
import sinon from 'sinon';
import { TYPE_TARGET_RDP } from 'api/models/target';
module('Acceptance | projects | sessions | session', function (hooks) {
setupApplicationTest(hooks);
@@ -24,6 +25,8 @@ module('Acceptance | projects | sessions | session', function (hooks) {
const TOAST_DO_NOT_SHOW_AGAIN_BUTTON =
'[data-test-toast-notification] button';
const TOAST_DISMISS_BUTTON = '[aria-label="Dismiss"]';
const RDP_OPEN_BUTTON = '[data-test-session-detail-open-button]';
const CANCEL_SESSION_BUTTON = '[data-test-session-detail-cancel-button]';
const instances = {
scopes: {
@@ -36,6 +39,7 @@ module('Acceptance | projects | sessions | session', function (hooks) {
},
user: null,
session: null,
rdpSession: null,
};
const urls = {
@@ -139,8 +143,8 @@ module('Acceptance | projects | sessions | session', function (hooks) {
this.ipcStub.withArgs('isCacheDaemonRunning').returns(false);
// mock RDP service calls
let rdpService = this.owner.lookup('service:rdp');
sinon.stub(rdpService, 'initialize').resolves();
this.rdpService = this.owner.lookup('service:rdp');
sinon.stub(this.rdpService, 'initialize').resolves();
});
hooks.afterEach(function () {
@@ -486,7 +490,7 @@ module('Acceptance | projects | sessions | session', function (hooks) {
await visit(urls.session);
assert.dom('[data-test-session-detail-cancel-button]').isVisible();
assert.dom(CANCEL_SESSION_BUTTON).isVisible();
});
test('cannot cancel a session without cancel permissions', async function (assert) {
@@ -504,7 +508,7 @@ module('Acceptance | projects | sessions | session', function (hooks) {
await visit(urls.session);
assert.dom('[data-test-session-detail-cancel-button]').isNotVisible();
assert.dom(CANCEL_SESSION_BUTTON).isNotVisible();
});
test('cancelling a session shows success alert', async function (assert) {
@@ -521,7 +525,7 @@ module('Acceptance | projects | sessions | session', function (hooks) {
await visit(urls.session);
await click('[data-test-session-detail-cancel-button]');
await click(CANCEL_SESSION_BUTTON);
assert
.dom('[data-test-toast-notification].hds-alert--color-success')
@@ -542,7 +546,7 @@ module('Acceptance | projects | sessions | session', function (hooks) {
await visit(urls.session);
await click('[data-test-session-detail-cancel-button]');
await click(CANCEL_SESSION_BUTTON);
assert.strictEqual(currentURL(), urls.targets);
});
@@ -562,7 +566,7 @@ module('Acceptance | projects | sessions | session', function (hooks) {
await visit(urls.session);
await click('[data-test-session-detail-cancel-button]');
await click(CANCEL_SESSION_BUTTON);
assert
.dom('[data-test-toast-notification].hds-alert--color-critical')
@@ -584,10 +588,109 @@ module('Acceptance | projects | sessions | session', function (hooks) {
await visit(urls.session);
await click('[data-test-session-detail-cancel-button]');
await click(CANCEL_SESSION_BUTTON);
assert
.dom('[data-test-toast-notification].hds-alert--color-critical')
.isVisible();
});
test('visiting an RDP session should display "open" button when preferred client is set', async function (assert) {
this.ipcStub.withArgs('cliExists').returns(true);
this.rdpService.preferredRdpClient = 'windows-app';
instances.target.update({ type: TYPE_TARGET_RDP });
this.ipcStub.withArgs('connect').returns({
session_id: instances.rdpSession.id,
host_id: 'h_123',
address: 'a_123',
port: 'p_123',
protocol: 'rdp',
});
await visit(urls.rdpTarget);
await click(TARGET_CONNECT_BUTTON);
assert.strictEqual(currentURL(), urls.rdpSession);
assert.dom(RDP_OPEN_BUTTON).isVisible();
await click(RDP_OPEN_BUTTON);
assert.ok(
this.ipcStub.calledWith('launchRdpClient', instances.rdpSession.id),
);
});
test('visiting an RDP session should not display "open" button when preferred client is set to none', async function (assert) {
this.ipcStub.withArgs('cliExists').returns(true);
this.rdpService.preferredRdpClient = 'none';
instances.target.update({ type: TYPE_TARGET_RDP });
this.ipcStub.withArgs('connect').returns({
session_id: instances.rdpSession.id,
host_id: 'h_123',
address: 'a_123',
port: 'p_123',
protocol: 'rdp',
});
await visit(urls.rdpTarget);
await click(TARGET_CONNECT_BUTTON);
assert.strictEqual(currentURL(), urls.rdpSession);
assert.dom(RDP_OPEN_BUTTON).doesNotExist();
});
test('it shows confirm modal when connection error occurs on launching rdp client', async function (assert) {
this.ipcStub.withArgs('cliExists').returns(true);
this.rdpService.preferredRdpClient = 'windows-app';
instances.target.update({ type: TYPE_TARGET_RDP });
this.ipcStub.withArgs('connect').returns({
session_id: instances.rdpSession.id,
host_id: 'h_123',
address: 'a_123',
port: 'p_123',
protocol: 'rdp',
});
this.ipcStub.withArgs('launchRdpClient', instances.rdpSession.id).rejects();
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
await visit(urls.rdpTarget);
await click(TARGET_CONNECT_BUTTON);
assert.strictEqual(currentURL(), urls.rdpSession);
assert.dom(RDP_OPEN_BUTTON).isVisible();
await click(RDP_OPEN_BUTTON);
assert.dom('.hds-modal').isVisible();
});
test('it displays open button without cancel session permission', async function (assert) {
this.ipcStub.withArgs('cliExists').returns(true);
this.rdpService.preferredRdpClient = 'windows-app';
instances.target.update({ type: TYPE_TARGET_RDP });
instances.rdpSession.update({ authorized_actions: [] });
this.ipcStub.withArgs('connect').returns({
session_id: instances.rdpSession.id,
host_id: 'h_123',
address: 'a_123',
port: 'p_123',
protocol: 'rdp',
});
await visit(urls.rdpTarget);
await click(TARGET_CONNECT_BUTTON);
assert.strictEqual(currentURL(), urls.rdpSession);
assert.dom(RDP_OPEN_BUTTON).isVisible();
assert.dom(CANCEL_SESSION_BUTTON).isNotVisible();
});
});
@@ -165,8 +165,8 @@ module('Acceptance | projects | targets | index', function (hooks) {
this.stubCacheDaemonSearch('sessions', 'targets', 'aliases', 'sessions');
// mock RDP service calls
let rdpService = this.owner.lookup('service:rdp');
sinon.stub(rdpService, 'initialize').resolves();
this.rdpService = this.owner.lookup('service:rdp');
sinon.stub(this.rdpService, 'initialize').resolves();
});
test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
@@ -719,8 +719,7 @@ module('Acceptance | projects | targets | index', function (hooks) {
});
test('shows `Open` button for RDP target with preferred client', async function (assert) {
let rdpService = this.owner.lookup('service:rdp');
rdpService.preferredRdpClient = 'windows-app';
this.rdpService.preferredRdpClient = 'windows-app';
instances.target.update({
type: TYPE_TARGET_RDP,
});
@@ -732,8 +731,7 @@ module('Acceptance | projects | targets | index', function (hooks) {
});
test('shows `Connect` button for RDP target with no preferred client', async function (assert) {
let rdpService = this.owner.lookup('service:rdp');
rdpService.preferredRdpClient = 'none';
this.rdpService.preferredRdpClient = 'none';
instances.target.update({
type: TYPE_TARGET_RDP,
});
@@ -754,8 +752,7 @@ module('Acceptance | projects | targets | index', function (hooks) {
test('clicking `Open` button for RDP target calls launchRdpClient IPC', async function (assert) {
this.ipcStub.withArgs('cliExists').returns(true);
const rdpService = this.owner.lookup('service:rdp');
rdpService.preferredRdpClient = 'windows-app';
this.rdpService.preferredRdpClient = 'windows-app';
instances.target.update({ type: TYPE_TARGET_RDP });
this.ipcStub.withArgs('connect').returns({
@@ -779,8 +776,7 @@ module('Acceptance | projects | targets | index', function (hooks) {
test('clicking `Connect` button for RDP target without preferred client calls connect IPC', async function (assert) {
this.ipcStub.withArgs('cliExists').returns(true);
const rdpService = this.owner.lookup('service:rdp');
rdpService.preferredRdpClient = 'none';
this.rdpService.preferredRdpClient = 'none';
instances.target.update({ type: TYPE_TARGET_RDP });
this.ipcStub.withArgs('connect').returns({
@@ -801,8 +797,7 @@ module('Acceptance | projects | targets | index', function (hooks) {
});
test('shows confirm modal when connection error occurs on launching rdp client', async function (assert) {
let rdpService = this.owner.lookup('service:rdp');
rdpService.preferredRdpClient = 'windows-app';
this.rdpService.preferredRdpClient = 'windows-app';
instances.target.update({ type: TYPE_TARGET_RDP });
this.ipcStub.withArgs('cliExists').returns(true);