diff --git a/addons/core/translations/actions/en-us.yaml b/addons/core/translations/actions/en-us.yaml
index 51178af71..5bf9accd6 100644
--- a/addons/core/translations/actions/en-us.yaml
+++ b/addons/core/translations/actions/en-us.yaml
@@ -68,3 +68,4 @@ show-errors: Show Errors
hide-errors: Hide Errors
edit-worker-filter: Edit Worker Filter
add-worker-filter: Add Worker Filter
+open: Open
diff --git a/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js b/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js
index e3470fa85..763425588 100644
--- a/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js
+++ b/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js
@@ -26,6 +26,7 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle
@service store;
@service can;
@service intl;
+ @service rdp;
// =attributes
@@ -244,6 +245,8 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle
'scopes.scope.projects.sessions.session',
session_id,
);
+
+ return session;
}
/**
@@ -311,4 +314,23 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle
async refresh() {
await this.currentRoute.refreshAll();
}
+
+ /**
+ * Quick connect method used to call main connect method and
+ * then launch RDP client
+ * @param {TargetModel} target
+ */
+ @action
+ async quickConnectAndLaunchRdp(target) {
+ try {
+ const session = await this.connect(target);
+ // Launch RDP client
+ await this.rdp.launchRdpClient(session.id);
+ } catch (error) {
+ this.confirm
+ .confirm(error.message, { isConnectError: true })
+ // Retry
+ .then(() => this.quickConnectAndLaunchRdp(target));
+ }
+ }
}
diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs
index 6092a0c71..d51d832b6 100644
--- a/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs
+++ b/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs
@@ -152,14 +152,25 @@
{{#if (can 'connect target' B.data)}}
{{#if (can 'read target' B.data)}}
-
+ {{#if (and B.data.isRDP this.rdp.isPreferredRdpClientSet)}}
+
+ {{else}}
+
+ {{/if}}
{{else}}
`[data-test-targets-open-button="${id}"]`;
+ const TARGET_CONNECT_BUTTON = (id) =>
+ `[data-test-targets-connect-button="${id}"]`;
const instances = {
scopes: {
global: null,
@@ -709,4 +712,111 @@ module('Acceptance | projects | targets | index', function (hooks) {
.dom(activeSessionFlyoutButtonSelector(instances.session.targetId))
.doesNotExist();
});
+
+ 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,
+ });
+ await visit(urls.targets);
+
+ assert.dom(TARGET_OPEN_BUTTON(instances.target.id)).exists();
+ assert.dom(TARGET_OPEN_BUTTON(instances.target.id)).hasText('Open');
+ assert.dom('[data-test-icon=external-link]').exists();
+ });
+
+ test('shows `Connect` button for RDP target with no preferred client', async function (assert) {
+ let rdpService = this.owner.lookup('service:rdp');
+ rdpService.preferredRdpClient = 'none';
+ instances.target.update({
+ type: TYPE_TARGET_RDP,
+ });
+ await visit(urls.targets);
+
+ assert.dom(TARGET_CONNECT_BUTTON(instances.target.id)).exists();
+ assert.dom(TARGET_CONNECT_BUTTON(instances.target.id)).hasText('Connect');
+ assert.dom('[data-test-icon=external-link]').doesNotExist();
+ });
+
+ test('shows `Connect` button for non-RDP target', async function (assert) {
+ await visit(urls.targets);
+
+ assert.dom(TARGET_CONNECT_BUTTON(instances.target.id)).exists();
+ assert.dom(TARGET_CONNECT_BUTTON(instances.target.id)).hasText('Connect');
+ });
+
+ 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';
+ instances.target.update({ type: TYPE_TARGET_RDP });
+
+ this.ipcStub.withArgs('connect').returns({
+ session_id: instances.session.id,
+ address: 'a_123',
+ port: 'p_123',
+ protocol: 'rdp',
+ });
+ this.ipcStub.withArgs('launchRdpClient').resolves();
+
+ // visit targets page
+ await visit(urls.targets);
+
+ assert.dom(TARGET_OPEN_BUTTON(instances.target.id)).exists();
+
+ await click(TARGET_OPEN_BUTTON(instances.target.id));
+
+ assert.ok(this.ipcStub.calledWith('launchRdpClient', instances.session.id));
+ });
+
+ 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';
+ instances.target.update({ type: TYPE_TARGET_RDP });
+
+ this.ipcStub.withArgs('connect').returns({
+ session_id: instances.session.id,
+ address: 'a_123',
+ port: 'p_123',
+ protocol: 'rdp',
+ });
+
+ // visit targets page
+ await visit(urls.targets);
+
+ assert.dom(TARGET_CONNECT_BUTTON(instances.target.id)).exists();
+
+ await click(TARGET_CONNECT_BUTTON(instances.target.id));
+
+ assert.ok(this.ipcStub.calledWith('connect'));
+ });
+
+ 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';
+ instances.target.update({ type: TYPE_TARGET_RDP });
+
+ this.ipcStub.withArgs('cliExists').returns(true);
+ // target quick connection is a success but launching RDP client fails
+ this.ipcStub.withArgs('connect').returns({
+ session_id: instances.session.id,
+ address: 'a_123',
+ port: 'p_123',
+ protocol: 'rdp',
+ });
+ this.ipcStub.withArgs('launchRdpClient').rejects();
+
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+
+ await visit(urls.targets);
+ await click(`[data-test-targets-open-button="${instances.target.id}"]`);
+
+ // Assert that the confirm modal appears
+ assert.dom(HDS_DIALOG_MODAL).isVisible();
+ });
});