diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml
index 7cb365157..597784975 100644
--- a/addons/core/translations/resources/en-us.yaml
+++ b/addons/core/translations/resources/en-us.yaml
@@ -369,6 +369,7 @@ session:
proxy-url:
address-port: Address & port
ssh: SSH
+ rdp: RDP
shell: Shell
global:
title: Global
diff --git a/ui/desktop/app/components/session/details/index.hbs b/ui/desktop/app/components/session/details/index.hbs
new file mode 100644
index 000000000..8e89058bb
--- /dev/null
+++ b/ui/desktop/app/components/session/details/index.hbs
@@ -0,0 +1,116 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+
+
+ {{#if @model.credentials}}
+
+ {{else}}
+
+
+
+
+ {{/if}}
+
+
+ {{#if (or @model.host @model.target)}}
+
+
+
+ {{/if}}
+
\ No newline at end of file
diff --git a/ui/desktop/app/components/session/proxy-url/index.hbs b/ui/desktop/app/components/session/proxy-url/index.hbs
index e3cc015e4..945e66fcd 100644
--- a/ui/desktop/app/components/session/proxy-url/index.hbs
+++ b/ui/desktop/app/components/session/proxy-url/index.hbs
@@ -3,40 +3,31 @@
SPDX-License-Identifier: BUSL-1.1
}}
-
+
-
- {{t 'resources.session.proxy-url.address-port'}}
- {{t 'resources.session.proxy-url.ssh'}}
+
+ {{#each this.dropdownOptions as |optionId|}}
+
+ {{t (concat 'resources.session.proxy-url.' optionId)}}
+
+ {{/each}}
- {{#if this.isAddressAndPort}}
-
-
- {{/if}}
- {{#unless this.isAddressAndPort}}
-
- {{/unless}}
+
+ {{#let this.copySnippetData as |snippet|}}
+ {{#if snippet}}
+
+ {{#if snippet.port}}
+
+ {{/if}}
+ {{/if}}
+ {{/let}}
\ No newline at end of file
diff --git a/ui/desktop/app/components/session/proxy-url/index.js b/ui/desktop/app/components/session/proxy-url/index.js
index b2cc084c8..5c2c302b0 100644
--- a/ui/desktop/app/components/session/proxy-url/index.js
+++ b/ui/desktop/app/components/session/proxy-url/index.js
@@ -7,28 +7,85 @@ import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { service } from '@ember/service';
+import {
+ TYPE_TARGET_RDP,
+ TYPE_TARGET_SSH,
+ TYPE_TARGET_TCP,
+} from 'api/models/target';
+
+const ADDRESS_PORT = 'address-port';
export default class SessionProxyUrlComponent extends Component {
+ // =services
+
@service intl;
- @tracked selectedValue = this.args.isSSHTarget
- ? this.intl.t('resources.session.proxy-url.ssh')
- : this.intl.t('resources.session.proxy-url.address-port');
+ // =attributes
- get isAddressAndPort() {
+ @tracked selectedValue = null;
+
+ /**
+ * Returns the current value to be displayed in the dropdown.
+ * If a value is selected, it returns that value.
+ * Otherwise, it defaults to 'address-port' for TCP targets or the targetType for others.
+ */
+ get currentValue() {
return (
- this.selectedValue ===
- this.intl.t('resources.session.proxy-url.address-port')
+ this.selectedValue ??
+ (this.args.targetType === TYPE_TARGET_TCP
+ ? ADDRESS_PORT
+ : this.args.targetType)
);
}
- get sshCommand() {
- return `ssh ${this.args.proxyAddress} -p ${this.args.proxyPort}`;
+ /**
+ * Returns the copy snippet data based on the current selected value.
+ */
+ get copySnippetData() {
+ const { proxyAddress, proxyPort } = this.args;
+ switch (this.currentValue) {
+ case ADDRESS_PORT:
+ return {
+ address: proxyAddress,
+ port: proxyPort,
+ };
+ case TYPE_TARGET_SSH:
+ return {
+ address: `ssh ${proxyAddress} -p ${proxyPort}`,
+ };
+ case TYPE_TARGET_RDP:
+ // TODO: This is a just a placeholder. Update this based on the design and product requirements.
+ return {
+ address: `rdp ${proxyAddress} -p ${proxyPort}`,
+ };
+ default:
+ return null;
+ }
}
+ /** Returns the dropdown options based on the target type.
+ */
+ get dropdownOptions() {
+ let options = [ADDRESS_PORT];
+
+ options.push(
+ this.args.targetType === TYPE_TARGET_RDP
+ ? TYPE_TARGET_RDP
+ : TYPE_TARGET_SSH,
+ );
+
+ return options;
+ }
+
+ // =actions
+
+ /** Updates the selected value and closes the dropdown.
+ * @param {string} optionId - The selected option.
+ * @param {function} closeCallback - The callback function to close the dropdown.
+ */
@action
- updateContent(value, closeCallback) {
- this.selectedValue = value;
+ handleSelect(optionId, closeCallback) {
+ this.selectedValue = optionId;
closeCallback();
}
}
diff --git a/ui/desktop/app/components/session/tabs/index.hbs b/ui/desktop/app/components/session/tabs/index.hbs
index b63774e6f..09cd3ab63 100644
--- a/ui/desktop/app/components/session/tabs/index.hbs
+++ b/ui/desktop/app/components/session/tabs/index.hbs
@@ -3,133 +3,37 @@
SPDX-License-Identifier: BUSL-1.1
}}
-
- {{t 'titles.details'}}
-
- {{t 'resources.session.shell'}}
-
-
- {{#if (and @model.proxy_address @model.proxy_port)}}
-
- {{/if}}
-
-
- {{#if @model.credentials}}
-
- {{else}}
-
-
-
-
- {{/if}}
-
+{{#if @model.target.isRDP}}
+ {{! RDP: Show content directly without tabs }}
+ {{#if (and @model.proxy_address @model.proxy_port)}}
+
+ {{/if}}
- {{#if (or @model.host @model.target)}}
-
-
-
+
+{{else}}
+ {{! Non-RDP: Show tabbed interface }}
+
+ {{t 'titles.details'}}
+
+ {{t 'resources.session.shell'}}
+
+
+ {{#if (and @model.proxy_address @model.proxy_port)}}
+
{{/if}}
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/desktop/app/styles/app.scss b/ui/desktop/app/styles/app.scss
index 42ef864c2..5196fd8c1 100644
--- a/ui/desktop/app/styles/app.scss
+++ b/ui/desktop/app/styles/app.scss
@@ -300,14 +300,6 @@
.hds-copy-snippet {
width: auto;
}
-
- .copy-address {
- max-width: 70%;
- }
-
- .copy-ssh-command {
- max-width: 90%;
- }
}
// Credentials Panel
diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/target/index.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/target/index.hbs
index b16433293..238f38ed6 100644
--- a/ui/desktop/app/templates/scopes/scope/projects/targets/target/index.hbs
+++ b/ui/desktop/app/templates/scopes/scope/projects/targets/target/index.hbs
@@ -134,12 +134,12 @@
{{/if}}
{{! Target type }}
-
- {{#if @model.target.isSSH}}
- {{t 'resources.target.types.ssh'}}
- {{else}}
- {{t 'resources.target.types.tcp'}}
- {{/if}}
+
+ {{t (concat 'resources.target.types.' @model.target.type)}}
{{! Target address }}
{{#if @model.target.address}}
diff --git a/ui/desktop/tests/integration/components/proxy-url-test.js b/ui/desktop/tests/integration/components/proxy-url-test.js
index 0a1e7e9e6..251d23121 100644
--- a/ui/desktop/tests/integration/components/proxy-url-test.js
+++ b/ui/desktop/tests/integration/components/proxy-url-test.js
@@ -8,90 +8,147 @@ import { setupRenderingTest } from 'desktop/tests/helpers';
import { render, click } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupIntl } from 'ember-intl/test-support';
+import {
+ TYPE_TARGET_SSH,
+ TYPE_TARGET_RDP,
+ TYPE_TARGET_TCP,
+} from 'api/models/target';
module('Integration | Component | session/proxy-url', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks, 'en-us');
- test('it renders', async function (assert) {
- assert.expect(2);
- this.set('proxyAddress', 'http://localhost');
- this.set('proxyPort', '1234');
+ test.each(
+ 'shows the correct command based on the target type',
+ [
+ {
+ targetType: TYPE_TARGET_SSH,
+ expectedCommand: (address, port) => `ssh ${address} -p ${port}`,
+ expectedSnippetCount: 1,
+ },
+ {
+ targetType: TYPE_TARGET_RDP,
+ expectedCommand: (address, port) => `rdp ${address} -p ${port}`,
+ expectedSnippetCount: 1,
+ },
+ {
+ targetType: TYPE_TARGET_TCP,
+ expectedCommand: (address, port) => `${address}:${port}`,
+ expectedSnippetCount: 2, // TCP has two copy snippets- one for address and one for port
+ },
+ ],
+ async function (
+ assert,
+ { targetType, expectedCommand, expectedSnippetCount },
+ ) {
+ this.set('proxyAddress', 'http://localhost');
+ this.set('proxyPort', '1234');
+ this.set('targetType', targetType);
- await render(
- hbs`
+ await render(
+ hbs`
`,
- );
+ );
+ assert.dom('.hds-copy-snippet').exists({ count: expectedSnippetCount });
- assert.dom('.copy-address').hasText(this.proxyAddress);
- assert.dom('.copy-port').hasText(this.proxyPort);
- });
+ // Assert the content of the copy snippets
+ if (targetType === TYPE_TARGET_TCP) {
+ assert
+ .dom('.hds-copy-snippet:nth-of-type(1)')
+ .hasText(this.proxyAddress);
+ assert.dom('.hds-copy-snippet:nth-of-type(2)').hasText(this.proxyPort);
+ } else {
+ assert
+ .dom('.hds-copy-snippet')
+ .hasText(expectedCommand(this.proxyAddress, this.proxyPort));
+ }
+ },
+ );
- test('it switches to SSH command on dropdown click', async function (assert) {
- assert.expect(2);
- this.set('proxyAddress', 'http://localhost');
- this.set('proxyPort', '1234');
+ test.each(
+ 'displays correct dropdown options based on target type',
+ [
+ {
+ targetType: TYPE_TARGET_SSH,
+ visibleOptions: [
+ '[data-test="ssh-option"]',
+ '[data-test="address-port-option"]',
+ ],
+ hiddenOptions: ['[data-test="rdp-option"]'],
+ },
+ {
+ targetType: TYPE_TARGET_RDP,
+ visibleOptions: [
+ '[data-test="rdp-option"]',
+ '[data-test="address-port-option"]',
+ ],
+ hiddenOptions: ['[data-test="ssh-option"]'],
+ },
+ {
+ targetType: TYPE_TARGET_TCP,
+ visibleOptions: [
+ '[data-test="address-port-option"]',
+ '[data-test="ssh-option"]',
+ ],
+ hiddenOptions: ['[data-test="rdp-option"]'],
+ },
+ ],
+ async function (assert, { targetType, visibleOptions, hiddenOptions }) {
+ this.set('proxyAddress', 'http://localhost');
+ this.set('proxyPort', '1234');
+ this.set('targetType', targetType);
- await render(
- hbs`
+ await render(
+ hbs`
`,
+ );
+
+ await click('.proxy-url-container .hds-dropdown-toggle-button');
+
+ // Assert visible options
+ for (const option of visibleOptions) {
+ assert.dom(option).isVisible();
+ }
+
+ // Assert hidden options
+ for (const option of hiddenOptions) {
+ assert.dom(option).doesNotExist();
+ }
+ },
+ );
+
+ test('updates command based on dropdown selection', async function (assert) {
+ this.set('proxyAddress', 'http://localhost');
+ this.set('proxyPort', '1234');
+ this.set('targetType', TYPE_TARGET_SSH);
+
+ await render(
+ hbs`
+
+ `,
);
+
+ // select address-port dropdown option
await click('.proxy-url-container .hds-dropdown-toggle-button');
- await click('[data-test-ssh-option]');
+ await click('[data-test="address-port-option"]');
- assert.dom('.copy-ssh-command').isVisible();
assert
- .dom('.copy-ssh-command')
- .hasText(`ssh ${this.proxyAddress} -p ${this.proxyPort}`);
- });
-
- test('shows address & port info when target type is TCP', async function (assert) {
- assert.expect(2);
- this.set('proxyAddress', 'http://localhost');
- this.set('proxyPort', '1234');
-
- await render(
- hbs`
-
- `,
- );
-
- assert.dom('.copy-address').hasText(this.proxyAddress);
- assert.dom('.copy-port').hasText(this.proxyPort);
- });
-
- test('shows ssh command when target type is SSH', async function (assert) {
- assert.expect(2);
- this.set('proxyAddress', 'http://localhost');
- this.set('proxyPort', '1234');
-
- await render(
- hbs`
-
- `,
- );
-
- assert.dom('.copy-ssh-command').isVisible();
- assert
- .dom('.copy-ssh-command')
- .hasText(`ssh ${this.proxyAddress} -p ${this.proxyPort}`);
+ .dom('.hds-copy-snippet:nth-of-type(1)')
+ .hasText(`${this.proxyAddress}`);
+ assert.dom('.hds-copy-snippet:nth-of-type(2)').hasText(`${this.proxyPort}`);
});
});
diff --git a/ui/desktop/tests/integration/components/session/tabs-test.js b/ui/desktop/tests/integration/components/session/tabs-test.js
new file mode 100644
index 000000000..ef0bc205e
--- /dev/null
+++ b/ui/desktop/tests/integration/components/session/tabs-test.js
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import {
+ TYPE_TARGET_SSH,
+ TYPE_TARGET_RDP,
+ TYPE_TARGET_TCP,
+} from 'api/models/target';
+import { setupIntl } from 'ember-intl/test-support';
+
+module('Integration | Component | session/tabs', function (hooks) {
+ setupRenderingTest(hooks);
+ setupIntl(hooks, 'en-us');
+
+ hooks.beforeEach(function () {
+ this.store = this.owner.lookup('service:store');
+ });
+
+ test.each(
+ 'it shows correct content for different target types',
+ [
+ {
+ targetType: TYPE_TARGET_TCP,
+ targetId: 'tcp-target-1',
+ defaultPort: 9000,
+ proxyPort: 9000,
+ shouldShowTabs: true,
+ },
+ {
+ targetType: TYPE_TARGET_SSH,
+ targetId: 'ssh-target-4',
+ defaultPort: 22,
+ proxyPort: 9000,
+ shouldShowTabs: true,
+ },
+ {
+ targetType: TYPE_TARGET_RDP,
+ targetId: 'rdp-target-2',
+ defaultPort: 3389,
+ proxyPort: 3389,
+ shouldShowTabs: false,
+ },
+ ],
+ async function (
+ assert,
+ { targetType, targetId, defaultPort, proxyPort, shouldShowTabs },
+ ) {
+ this.store.createRecord('target', {
+ id: targetId,
+ type: targetType,
+ address: 'example.com',
+ default_port: defaultPort,
+ scope: {
+ scope_id: 'p_123',
+ type: 'project',
+ },
+ });
+
+ const session = this.store.createRecord('session', {
+ id: `session-${targetId}`,
+ target_id: targetId,
+ proxy_address: '127.0.0.1',
+ proxy_port: proxyPort,
+ });
+
+ this.set('model', session);
+
+ await render(hbs``);
+
+ if (shouldShowTabs) {
+ assert.dom('.hds-tabs').exists();
+ } else {
+ assert.dom('.hds-tabs').doesNotExist();
+ }
+
+ assert.dom('[data-test-proxy-url]').exists();
+ assert.dom('[data-test-session-details]').exists();
+ },
+ );
+});