chore: 🤖 rdp desktop changes

This commit is contained in:
Priya Patel authored and Dharini committed 2025-09-03 16:03:48 -07:00
1 parent 42e32f9568
commit 4a179ac3e3
9 files changed
+452 -248

No files matched your search

@@ -369,6 +369,7 @@ session:
proxy-url:
address-port: Address & port
ssh: SSH
rdp: RDP
shell: Shell
global:
title: Global
@@ -0,0 +1,116 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Rose::Layout::BodyContent data-test-session-details as |bc|>
<bc.Body>
{{#if @model.credentials}}
<CredentialsPanel @credentials={{@model.credentials}} />
{{else}}
<Hds::ApplicationState data-test-no-credentials as |A|>
<A.Header @title={{t 'resources.session.messages.connected.title'}} />
<A.Body
@text={{t
'resources.session.messages.connected.description'
targetDisplayName=@model.target.displayName
}}
/>
</Hds::ApplicationState>
{{/if}}
</bc.Body>
{{#if (or @model.host @model.target)}}
<bc.Sidebar>
<div class='details-sidebar'>
{{#if @model.host}}
<div
class='title hds-typography-display-200 hds-font-weight-semibold'
>
{{t 'resources.host.details'}}
</div>
<Rose::MetadataList @orientation='vertical' as |list|>
{{! Host name }}
{{#if @model.host.name}}
<list.Item
@icon='monitor'
@color='faint'
>{{@model.host.name}}</list.Item>
{{/if}}
{{! Host description }}
{{#if @model.host.description}}
<list.Item
@icon='message-circle-fill'
@color='faint'
>{{@model.host.description}}</list.Item>
{{/if}}
{{! Host address }}
{{#if @model.host.address}}
<list.Item
@icon='network-alt'
@color='faint'
>{{@model.host.address}}</list.Item>
{{/if}}
{{! Host ID }}
<list.Item @icon='hash' @color='faint'>
<Hds::Copy::Snippet
@textToCopy={{@model.host.id}}
@color='secondary'
/>
</list.Item>
</Rose::MetadataList>
{{/if}}
{{#if @model.target}}
<div
class='title hds-typography-display-200 hds-font-weight-semibold'
>
{{t 'resources.target.details'}}
</div>
<Rose::MetadataList @orientation='vertical' as |list|>
{{! Target name }}
<list.Item
@icon='target'
@color='faint'
>{{@model.target.name}}</list.Item>
{{! Project name }}
<list.Item
@icon='grid'
@color='faint'
>{{@model.target.project.displayName}}</list.Item>
{{! Target description }}
{{#if @model.target.description}}
<list.Item
@icon='message-circle-fill'
@color='faint'
>{{@model.target.description}}</list.Item>
{{/if}}
{{! Target type (SSH/RDP/TCP) }}
<list.Item
class='resource-type'
@icon={{if @model.target.isRDP 'microsoft' 'terminal'}}
@color='faint'
>
{{t (concat 'resources.target.types.' @model.target.type)}}
</list.Item>
{{! Target address }}
{{#if @model.target.address}}
<list.Item
class='resource-type'
@icon='network-alt'
@color='faint'
>{{@model.target.address}}</list.Item>
{{/if}}
{{! Target ID}}
<list.Item @icon='hash' @color='faint'>
<Hds::Copy::Snippet
@textToCopy={{@model.target.id}}
@color='secondary'
/>
</list.Item>
</Rose::MetadataList>
{{/if}}
</div>
</bc.Sidebar>
{{/if}}
</Rose::Layout::BodyContent>
@@ -3,40 +3,31 @@
SPDX-License-Identifier: BUSL-1.1
}}
<div class='proxy-url-container'>
<div class='proxy-url-container' data-test-proxy-url>
<Hds::Dropdown @listPosition='bottom-left' as |dd|>
<dd.ToggleButton @color='secondary' @text={{this.selectedValue}} />
<dd.Interactive
{{on
'click'
(fn
this.updateContent
(t 'resources.session.proxy-url.address-port')
dd.close
)
}}
>{{t 'resources.session.proxy-url.address-port'}}</dd.Interactive>
<dd.Interactive
data-test-ssh-option
{{on
'click'
(fn this.updateContent (t 'resources.session.proxy-url.ssh') dd.close)
}}
>{{t 'resources.session.proxy-url.ssh'}}</dd.Interactive>
<dd.ToggleButton
@color='secondary'
@text={{t (concat 'resources.session.proxy-url.' this.currentValue)}}
/>
{{#each this.dropdownOptions as |optionId|}}
<dd.Interactive
data-test={{concat optionId '-option'}}
{{on 'click' (fn this.handleSelect optionId dd.close)}}
>
{{t (concat 'resources.session.proxy-url.' optionId)}}
</dd.Interactive>
{{/each}}
</Hds::Dropdown>
{{#if this.isAddressAndPort}}
<Hds::Copy::Snippet
class='copy-address'
@textToCopy={{@proxyAddress}}
@isTruncated={{true}}
/>
<Hds::Copy::Snippet class='copy-port' @textToCopy={{@proxyPort}} />
{{/if}}
{{#unless this.isAddressAndPort}}
<Hds::Copy::Snippet
class='copy-ssh-command'
@textToCopy={{this.sshCommand}}
@isTruncated={{true}}
/>
{{/unless}}
{{#let this.copySnippetData as |snippet|}}
{{#if snippet}}
<Hds::Copy::Snippet
@textToCopy={{snippet.address}}
@isTruncated={{true}}
/>
{{#if snippet.port}}
<Hds::Copy::Snippet @textToCopy={{snippet.port}} />
{{/if}}
{{/if}}
{{/let}}
</div>
@@ -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();
}
}
+32 -128
View File
@@ -3,133 +3,37 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Tabs as |T|>
<T.Tab>{{t 'titles.details'}} </T.Tab>
<T.Tab @icon='terminal' {{on 'click' this.openTerminal}}>
{{t 'resources.session.shell'}}
</T.Tab>
<T.Panel>
{{#if (and @model.proxy_address @model.proxy_port)}}
<Session::ProxyUrl
@proxyAddress={{@model.proxy_address}}
@proxyPort={{@model.proxy_port}}
@isSSHTarget={{@model.target.isSSH}}
/>
{{/if}}
<Rose::Layout::BodyContent as |bc|>
<bc.Body>
{{#if @model.credentials}}
<CredentialsPanel @credentials={{@model.credentials}} />
{{else}}
<Hds::ApplicationState data-test-no-credentials as |A|>
<A.Header
@title={{t 'resources.session.messages.connected.title'}}
/>
<A.Body
@text={{t
'resources.session.messages.connected.description'
targetDisplayName=@model.target.displayName
}}
/>
</Hds::ApplicationState>
{{/if}}
</bc.Body>
{{#if @model.target.isRDP}}
{{! RDP: Show content directly without tabs }}
{{#if (and @model.proxy_address @model.proxy_port)}}
<Session::ProxyUrl
@proxyAddress={{@model.proxy_address}}
@proxyPort={{@model.proxy_port}}
@targetType={{@model.target.type}}
/>
{{/if}}
{{#if (or @model.host @model.target)}}
<bc.Sidebar>
<div class='details-sidebar'>
{{#if @model.host}}
<div
class='title hds-typography-display-200 hds-font-weight-semibold'
>
{{t 'resources.host.details'}}
</div>
<Rose::MetadataList @orientation='vertical' as |list|>
{{! Host name }}
{{#if @model.host.name}}
<list.Item @icon='monitor' @color='faint'>
{{@model.host.name}}
</list.Item>
{{/if}}
{{! Host description }}
{{#if @model.host.description}}
<list.Item @icon='message-circle-fill' @color='faint'>
{{@model.host.description}}
</list.Item>
{{/if}}
{{! Host address }}
{{#if @model.host.address}}
<list.Item @icon='network-alt' @color='faint'>
{{@model.host.address}}
</list.Item>
{{/if}}
{{! Host ID }}
<list.Item @icon='hash' @color='faint'>
<Hds::Copy::Snippet
@textToCopy={{@model.host.id}}
@color='secondary'
/>
</list.Item>
</Rose::MetadataList>
{{/if}}
{{#if @model.target}}
<div
class='title hds-typography-display-200 hds-font-weight-semibold'
>
{{t 'resources.target.details'}}
</div>
<Rose::MetadataList @orientation='vertical' as |list|>
{{! Target name }}
<list.Item @icon='target' @color='faint'>
{{@model.target.name}}
</list.Item>
{{! Project name }}
<list.Item @icon='grid' @color='faint'>
{{@model.target.project.displayName}}
</list.Item>
{{! Target description }}
{{#if @model.target.description}}
<list.Item @icon='message-circle-fill' @color='faint'>
{{@model.target.description}}
</list.Item>
{{/if}}
{{! Target type }}
<list.Item
class='resource-type'
@icon='terminal'
@color='faint'
>
{{#if @model.target.isSSH}}
{{t 'resources.target.types.ssh'}}
{{else}}
{{t 'resources.target.types.tcp'}}
{{/if}}
</list.Item>
{{! Target address }}
{{#if @model.target.address}}
<list.Item
class='resource-type'
@icon='network-alt'
@color='faint'
>
{{@model.target.address}}
</list.Item>
{{/if}}
{{! Target ID }}
<list.Item @icon='hash' @color='faint'>
<Hds::Copy::Snippet
@textToCopy={{@model.target.id}}
@color='secondary'
/>
</list.Item>
</Rose::MetadataList>
{{/if}}
</div>
</bc.Sidebar>
<Session::Details @model={{@model}} />
{{else}}
{{! Non-RDP: Show tabbed interface }}
<Hds::Tabs as |T|>
<T.Tab>{{t 'titles.details'}}</T.Tab>
<T.Tab @icon='terminal' {{on 'click' this.openTerminal}}>
{{t 'resources.session.shell'}}
</T.Tab>
<T.Panel>
{{#if (and @model.proxy_address @model.proxy_port)}}
<Session::ProxyUrl
@proxyAddress={{@model.proxy_address}}
@proxyPort={{@model.proxy_port}}
@targetType={{@model.target.type}}
/>
{{/if}}
</Rose::Layout::BodyContent>
</T.Panel>
<T.Panel>
<div id='terminal-container'></div>
</T.Panel>
</Hds::Tabs>
<Session::Details @model={{@model}} />
</T.Panel>
<T.Panel>
<div id='terminal-container'></div>
</T.Panel>
</Hds::Tabs>
{{/if}}
-8
View File
@@ -300,14 +300,6 @@
.hds-copy-snippet {
width: auto;
}
.copy-address {
max-width: 70%;
}
.copy-ssh-command {
max-width: 90%;
}
}
// Credentials Panel
@@ -134,12 +134,12 @@
</list.Item>
{{/if}}
{{! Target type }}
<list.Item class='resource-type' @icon='terminal' @color='faint'>
{{#if @model.target.isSSH}}
{{t 'resources.target.types.ssh'}}
{{else}}
{{t 'resources.target.types.tcp'}}
{{/if}}
<list.Item
class='resource-type'
@icon={{if @model.target.isRDP 'microsoft' 'terminal'}}
@color='faint'
>
{{t (concat 'resources.target.types.' @model.target.type)}}
</list.Item>
{{! Target address }}
{{#if @model.target.address}}
@@ -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`
<Session::ProxyUrl
@proxyAddress={{this.proxyAddress}}
@proxyPort={{this.proxyPort}}
@isSSHTarget={{false}}
@targetType={{this.targetType}}
/>
`,
);
);
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`
<Session::ProxyUrl
@proxyAddress={{this.proxyAddress}}
@proxyPort={{this.proxyPort}}
@isSSHTarget={{false}}
@targetType={{this.targetType}}
/>
`,
);
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`
<Session::ProxyUrl
@proxyAddress={{this.proxyAddress}}
@proxyPort={{this.proxyPort}}
@targetType={{this.targetType}}
/>
`,
);
// 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`
<Session::ProxyUrl
@proxyAddress={{this.proxyAddress}}
@proxyPort={{this.proxyPort}}
@isSSHTarget={{false}}
/>
`,
);
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`
<Session::ProxyUrl
@proxyAddress={{this.proxyAddress}}
@proxyPort={{this.proxyPort}}
@isSSHTarget={{true}}
/>
`,
);
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}`);
});
});
@@ -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`<Session::Tabs @model={{this.model}} />`);
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();
},
);
});